diff --git a/components/input/OTP/index.tsx b/components/input/OTP/index.tsx index bfc7e92df2..c66f3538db 100644 --- a/components/input/OTP/index.tsx +++ b/components/input/OTP/index.tsx @@ -17,9 +17,15 @@ import type { InputRef } from '../Input'; import useStyle from '../style/otp'; import OTPInput from './OTPInput'; import type { OTPInputProps } from './OTPInput'; +import type { + SemanticClassNamesType, + SemanticStylesType, +} from '../../_util/hooks/useMergeSemantic'; type SemanticName = 'root' | 'input' | 'separator'; +export type OTPClassNamesType = SemanticClassNamesType; +export type OTPStylesType = SemanticStylesType; export interface OTPRef { focus: VoidFunction; blur: VoidFunction; @@ -55,8 +61,8 @@ export interface OTPProps onInput?: (value: string[]) => void; - classNames?: Partial>; - styles?: Partial>; + classNames?: OTPClassNamesType; + styles?: OTPStylesType; } function strToArr(str: string) { @@ -128,10 +134,16 @@ const OTP = React.forwardRef((props, ref) => { } = useComponentConfig('otp'); const prefixCls = getPrefixCls('otp', customizePrefixCls); - const [mergedClassNames, mergedStyles] = useMergeSemantic( - [contextClassNames, classNames], - [contextStyles, styles], - ); + const mergedProps: OTPProps = { + ...props, + length, + }; + + const [mergedClassNames, mergedStyles] = useMergeSemantic< + OTPClassNamesType, + OTPStylesType, + OTPProps + >([contextClassNames, classNames], [contextStyles, styles], undefined, { props: mergedProps }); const domAttrs = pickAttrs(restProps, { aria: true, diff --git a/components/input/Search.tsx b/components/input/Search.tsx index b711090987..eb18cb9b48 100644 --- a/components/input/Search.tsx +++ b/components/input/Search.tsx @@ -4,6 +4,8 @@ import { composeRef } from '@rc-component/util/lib/ref'; import cls from 'classnames'; import useMergeSemantic from '../_util/hooks/useMergeSemantic'; +import type { SemanticClassNamesType, SemanticStylesType } from '../_util/hooks/useMergeSemantic'; + import { cloneElement } from '../_util/reactNode'; import Button from '../button'; import type { ButtonSemanticName } from '../button/button'; @@ -15,6 +17,12 @@ import Input from './Input'; type SemanticName = 'root' | 'input' | 'prefix' | 'suffix' | 'count'; +export type InputSearchClassNamesType = SemanticClassNamesType & { + button?: Partial>; +}; +export type InputSearchStylesType = SemanticStylesType & { + button?: Partial>; +}; export interface SearchProps extends InputProps { inputPrefixCls?: string; onSearch?: ( @@ -30,12 +38,8 @@ export interface SearchProps extends InputProps { enterButton?: React.ReactNode; loading?: boolean; onPressEnter?: (e: React.KeyboardEvent) => void; - classNames?: Partial> & { - button?: Partial>; - }; - styles?: Partial> & { - button?: Partial>; - }; + classNames?: InputSearchClassNamesType; + styles?: InputSearchStylesType; } const Search = React.forwardRef((props, ref) => { @@ -67,7 +71,15 @@ const Search = React.forwardRef((props, ref) => { styles: contextStyles, } = useComponentConfig('inputSearch'); - const [mergedClassNames, mergedStyles] = useMergeSemantic( + const mergedProps: SearchProps = { + ...props, + enterButton, + }; + const [mergedClassNames, mergedStyles] = useMergeSemantic< + InputSearchClassNamesType, + InputSearchStylesType, + SearchProps + >( [contextClassNames, classNames], [contextStyles, styles], { @@ -75,6 +87,7 @@ const Search = React.forwardRef((props, ref) => { _default: 'root', }, }, + { props: mergedProps }, ); const composedRef = React.useRef(false); diff --git a/components/input/TextArea.tsx b/components/input/TextArea.tsx index 4f634ba03c..a887c6aad8 100644 --- a/components/input/TextArea.tsx +++ b/components/input/TextArea.tsx @@ -25,10 +25,13 @@ import { triggerFocus } from './Input'; import { useSharedStyle } from './style'; import useStyle from './style/textarea'; import useMergeSemantic from '../_util/hooks/useMergeSemantic'; +import type { SemanticClassNamesType, SemanticStylesType } from '../_util/hooks/useMergeSemantic'; type SemanticName = 'root' | 'textarea' | 'count'; +export type TextAreaClassNamesType = SemanticClassNamesType; +export type TextAreaStylesType = SemanticStylesType; -export interface TextAreaProps extends Omit { +export interface TextAreaProps extends Omit { /** @deprecated Use `variant` instead */ bordered?: boolean; size?: SizeType; @@ -39,8 +42,8 @@ export interface TextAreaProps extends Omit { * @default "outlined" */ variant?: Variant; - classNames?: Partial>; - styles?: Partial>; + classNames?: TextAreaClassNamesType; + styles?: TextAreaStylesType; } export interface TextAreaRef { @@ -97,10 +100,11 @@ const TextArea = forwardRef((props, ref) => { } = React.useContext(FormItemInputContext); const mergedStatus = getMergedStatus(contextStatus, customStatus); - const [mergedClassNames, mergedStyles] = useMergeSemantic( - [contextClassNames, classNames], - [contextStyles, styles], - ); + const [mergedClassNames, mergedStyles] = useMergeSemantic< + TextAreaClassNamesType, + TextAreaStylesType, + TextAreaProps + >([contextClassNames, classNames], [contextStyles, styles], undefined, { props }); // ===================== Ref ====================== const innerRef = React.useRef(null); diff --git a/components/input/__tests__/Search.test.tsx b/components/input/__tests__/Search.test.tsx index 2cc79bb7a8..8a6334db01 100644 --- a/components/input/__tests__/Search.test.tsx +++ b/components/input/__tests__/Search.test.tsx @@ -7,6 +7,8 @@ import rtlTest from '../../../tests/shared/rtlTest'; import Button from '../../button'; import type { InputRef } from '../Input'; import Search from '../Search'; +import type { SearchProps } from '../Search'; +import { EditOutlined, UserOutlined } from '@ant-design/icons'; describe('Input.Search', () => { focusTest(Search, { refFocus: true }); @@ -227,4 +229,136 @@ describe('Input.Search', () => { fireEvent.keyDown(container.querySelector('input')!, { key: 'Enter', keyCode: 13 }); expect(onPressEnter).toHaveBeenCalledTimes(1); }); + + it('support function classNames and styles', () => { + const functionClassNames = (info: { props: SearchProps }) => { + const { props } = info; + const { enterButton, disabled } = props; + return { + root: 'dynamic-root', + input: enterButton ? 'dynamic-input-with-button' : 'dynamic-input-without-button', + prefix: 'dynamic-prefix', + suffix: 'dynamic-suffix', + count: 'dynamic-count', + button: { + root: 'dynamic-button-root', + icon: disabled ? 'dynamic-button-icon-disabled' : 'dynamic-button-icon', + }, + }; + }; + const functionStyles = (info: { props: SearchProps }) => { + const { props } = info; + const { enterButton, disabled } = props; + return { + root: { color: 'rgb(255, 0, 0)' }, + input: { color: enterButton ? 'rgb(0, 255, 0)' : 'rgb(255, 0, 0)' }, + prefix: { color: 'rgb(0, 0, 255)' }, + suffix: { color: 'rgb(255, 0, 0)' }, + count: { color: 'rgb(255, 0, 0)' }, + button: { + root: { color: 'rgb(0, 255, 0)' }, + icon: { color: disabled ? 'rgb(0, 0, 255)' : 'rgb(255, 0, 0)' }, + }, + }; + }; + const { container, rerender } = render( + } + suffix={} + defaultValue="Hello, Ant Design" + classNames={functionClassNames} + styles={functionStyles} + disabled + />, + ); + const root = container.querySelector('.ant-input-search'); + const input = container.querySelector('.ant-input'); + const prefix = container.querySelector('.ant-input-prefix'); + const suffix = container.querySelector('.ant-input-suffix'); + const count = container.querySelector('.ant-input-show-count-suffix'); + const button = container.querySelector('.ant-btn'); + const buttonIcon = container.querySelector('.ant-btn-icon'); + + expect(root).toHaveClass('dynamic-root'); + expect(input).toHaveClass('dynamic-input-without-button'); + expect(prefix).toHaveClass('dynamic-prefix'); + expect(suffix).toHaveClass('dynamic-suffix'); + expect(count).toHaveClass('dynamic-count'); + expect(button).toHaveClass('dynamic-button-root'); + expect(buttonIcon).toHaveClass('dynamic-button-icon-disabled'); + + expect(root).toHaveStyle('color: rgb(255, 0, 0)'); + expect(input).toHaveStyle('color: rgb(255, 0, 0)'); + expect(prefix).toHaveStyle('color: rgb(0, 0, 255)'); + expect(suffix).toHaveStyle('color: rgb(255, 0, 0)'); + expect(count).toHaveStyle('color: rgb(255, 0, 0)'); + expect(button).toHaveStyle('color: rgb(0, 255, 0)'); + expect(buttonIcon).toHaveStyle('color: rgb(0, 0, 255)'); + + const objectClassNames = { + root: 'dynamic-root-default', + input: 'dynamic-input-default', + prefix: 'dynamic-prefix-default', + suffix: 'dynamic-suffix-default', + count: 'dynamic-count-default', + }; + const objectStyles = { + root: { color: 'rgb(255, 0, 0)' }, + input: { color: 'rgb(0, 255, 0)' }, + prefix: { color: 'rgb(0, 0, 255)' }, + suffix: { color: 'rgb(0, 255, 0)' }, + count: { color: 'rgb(0, 255, 0)' }, + }; + const objectButtonClassNames = { + root: 'dynamic-custom-button-root', + icon: 'dynamic-custom-button-icon', + content: 'dynamic-custom-button-content', + }; + const objectButtonStyles = { + root: { color: 'rgb(0, 255, 0)' }, + icon: { color: 'rgb(255, 0, 0)' }, + content: { color: 'rgb(0, 255, 0)' }, + }; + rerender( + } + suffix={} + defaultValue="Hello, Ant Design" + classNames={objectClassNames} + styles={objectStyles} + disabled + enterButton={ + + } + />, + ); + + const buttonContent = container.querySelector('.ant-btn > .ant-btn-icon + span'); + + expect(root).toHaveClass('dynamic-root-default'); + expect(input).toHaveClass('dynamic-input-default'); + expect(prefix).toHaveClass('dynamic-prefix-default'); + expect(suffix).toHaveClass('dynamic-suffix-default'); + expect(count).toHaveClass('dynamic-count-default'); + expect(button).toHaveClass('dynamic-custom-button-root'); + expect(buttonIcon).toHaveClass('dynamic-custom-button-icon'); + expect(buttonContent).toHaveClass('dynamic-custom-button-content'); + + expect(root).toHaveStyle('color: rgb(255, 0, 0)'); + expect(input).toHaveStyle('color: rgb(0, 255, 0)'); + expect(prefix).toHaveStyle('color: rgb(0, 0, 255)'); + expect(suffix).toHaveStyle('color: rgb(0, 255, 0)'); + expect(count).toHaveStyle('color: rgb(0, 255, 0)'); + expect(button).toHaveStyle('color: rgb(0, 255, 0)'); + expect(buttonIcon).toHaveStyle('color: rgb(255, 0, 0)'); + expect(buttonContent).toHaveStyle('color: rgb(0, 255, 0)'); + }); }); diff --git a/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap index bc2c59a090..b2d8131a3b 100644 --- a/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -12183,52 +12183,237 @@ exports[`renders components/input/demo/status.tsx extend context correctly 2`] = exports[`renders components/input/demo/style-class.tsx extend context correctly 1`] = `
-
+ + -
+ TextArea + + + + 8 + + + + + + + + + + + +
+ - -
-
-
-
+ + * + + + + + * + + -
+ + + * + + + + + + * + + + + + + * + + + +
+ + + + + + + +
`; diff --git a/components/input/__tests__/__snapshots__/demo.test.tsx.snap b/components/input/__tests__/__snapshots__/demo.test.tsx.snap index 0438304b40..03320c59aa 100644 --- a/components/input/__tests__/__snapshots__/demo.test.tsx.snap +++ b/components/input/__tests__/__snapshots__/demo.test.tsx.snap @@ -5542,52 +5542,237 @@ exports[`renders components/input/demo/status.tsx correctly 1`] = ` exports[`renders components/input/demo/style-class.tsx correctly 1`] = `
-
+ + -
+ TextArea + + + + 8 + + + + + + + + + + + +
+ - -
-
-
-
+ + * + + + + + * + + -
+ + + * + + + + + + * + + + + + + * + + + +
+ + + + + + + +
`; diff --git a/components/input/__tests__/otp.test.tsx b/components/input/__tests__/otp.test.tsx index 912aa1f0f9..3befa90150 100644 --- a/components/input/__tests__/otp.test.tsx +++ b/components/input/__tests__/otp.test.tsx @@ -5,6 +5,7 @@ import focusTest from '../../../tests/shared/focusTest'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; import { createEvent, fireEvent, render, waitFakeTimer } from '../../../tests/utils'; +import type { OTPProps } from '../OTP'; const { OTP } = Input; @@ -237,4 +238,65 @@ describe('Input.OTP', () => { expect(separator.textContent).toBe('X'); }); }); + + it('support function classNames and styles', () => { + const functionClassNames = (info: { props: OTPProps }) => { + const { props } = info; + const { disabled } = props; + return { + root: 'dynamic-root', + input: disabled ? 'dynamic-input-disabled' : 'dynamic-input-enabled', + separator: 'dynamic-separator', + }; + }; + const functionStyles = (info: { props: OTPProps }) => { + const { props } = info; + const { disabled } = props; + return { + root: { color: 'rgb(255, 0, 0)' }, + input: { color: disabled ? 'rgb(0, 255, 0)' : 'rgb(255, 0, 0)' }, + separator: { color: 'rgb(0, 0, 255)' }, + }; + }; + const { container, rerender } = render( + , + ); + const root = container.querySelector('.ant-otp'); + const input = container.querySelector('.ant-input'); + const separator = container.querySelector('.ant-otp-separator'); + + expect(root).toHaveClass('dynamic-root'); + expect(input).toHaveClass('dynamic-input-disabled'); + expect(separator).toHaveClass('dynamic-separator'); + + expect(root).toHaveStyle('color: rgb(255, 0, 0)'); + expect(input).toHaveStyle('color: rgb(0, 255, 0)'); + expect(separator).toHaveStyle('color: rgb(0, 0, 255)'); + + const objectClassNames = { + root: 'dynamic-root-default', + input: 'dynamic-input-enabled', + separator: 'dynamic-separator-default', + }; + const objectStyles = { + root: { color: 'rgb(0, 255, 0)' }, + input: { color: 'rgb(255, 0, 0)' }, + separator: { color: 'rgb(0, 0, 255)' }, + }; + + rerender(); + + expect(root).toHaveClass('dynamic-root-default'); + expect(input).toHaveClass('dynamic-input-enabled'); + expect(separator).toHaveClass('dynamic-separator-default'); + expect(root).toHaveStyle('color: rgb(0, 255, 0)'); + expect(input).toHaveStyle('color: rgb(255, 0, 0)'); + expect(separator).toHaveStyle('color: rgb(0, 0, 255)'); + }); }); diff --git a/components/input/__tests__/textarea.test.tsx b/components/input/__tests__/textarea.test.tsx index cfcb28f21e..5087c04659 100644 --- a/components/input/__tests__/textarea.test.tsx +++ b/components/input/__tests__/textarea.test.tsx @@ -12,7 +12,7 @@ import { waitFakeTimer, waitFakeTimer19, } from '../../../tests/utils'; -import type { TextAreaRef } from '../TextArea'; +import type { TextAreaProps, TextAreaRef } from '../TextArea'; const { TextArea } = Input; @@ -245,6 +245,72 @@ describe('TextArea', () => { expect(ref.current?.resizableTextArea?.textArea.selectionStart).toEqual(5); expect(ref.current?.resizableTextArea?.textArea.selectionEnd).toEqual(5); }); + + it('support function classNames and styles', () => { + const functionClassNames: TextAreaProps['classNames'] = (info) => { + const { props } = info; + return { + root: 'dynamic-root', + textarea: props.disabled ? 'disabled-item' : 'enabled-item', + count: `dynamic-count-${props.count?.max}`, + }; + }; + + const functionStyles: TextAreaProps['styles'] = (info) => { + const { props } = info; + return { + root: { + backgroundColor: props.size === 'small' ? '#e6f7ff' : '#f6ffed', + }, + textarea: { + color: props.disabled ? '#d9d9d9' : '#52c41a', + }, + count: { + color: props.count?.max === 1024 ? '#e6f7ff' : '#f6ffed', + }, + }; + }; + + const { container, rerender } = render( +