diff --git a/components/_util/hooks/useConfigInject.ts b/components/_util/hooks/useConfigInject.ts index 7bde67632..256dfb922 100644 --- a/components/_util/hooks/useConfigInject.ts +++ b/components/_util/hooks/useConfigInject.ts @@ -24,6 +24,8 @@ export default ( virtual: ComputedRef; dropdownMatchSelectWidth: ComputedRef; getPopupContainer: ComputedRef; + getPrefixCls: ConfigProviderProps['getPrefixCls']; + autocomplete: ComputedRef; } => { const configProvider = inject>( 'configProvider', @@ -48,6 +50,7 @@ export default ( () => props.dropdownMatchSelectWidth ?? configProvider.dropdownMatchSelectWidth, ); const size = computed(() => props.size || configProvider.componentSize); + const autocomplete = computed(() => props.autocomplete || configProvider.input?.autocomplete); return { configProvider, prefixCls, @@ -63,5 +66,7 @@ export default ( virtual, dropdownMatchSelectWidth, rootPrefixCls, + getPrefixCls: configProvider.getPrefixCls, + autocomplete, }; }; diff --git a/components/_util/util.ts b/components/_util/util.ts index 48a604753..1adfa26ef 100644 --- a/components/_util/util.ts +++ b/components/_util/util.ts @@ -1,6 +1,6 @@ import type { VueNode } from './type'; export const isFunction = val => typeof val === 'function'; - +export const controlDefaultValue = Symbol('controlDefaultValue') as any; export const isArray = Array.isArray; export const isString = val => typeof val === 'string'; export const isSymbol = val => typeof val === 'symbol'; diff --git a/components/button/button.tsx b/components/button/button.tsx index 7552eb667..7cc13edeb 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -35,7 +35,7 @@ export default defineComponent({ __ANT_BUTTON: true, props, slots: ['icon'], - emits: ['click'], + emits: ['click', 'mousedown'], setup(props, { slots, attrs, emit }) { const { prefixCls, autoInsertSpaceInButton, direction } = useConfigInject('btn', props); diff --git a/components/config-provider/index.tsx b/components/config-provider/index.tsx index 23a9ec984..07ba5f5c4 100644 --- a/components/config-provider/index.tsx +++ b/components/config-provider/index.tsx @@ -160,6 +160,9 @@ export const configProviderProps = { csp: { type: Object as PropType, }, + input: { + type: Object as PropType<{ autocomplete: string }>, + }, autoInsertSpaceInButton: PropTypes.looseBool, locale: { type: Object as PropType, diff --git a/components/input/ClearableLabeledInput.tsx b/components/input/ClearableLabeledInput.tsx index 5eb640f8c..89f1ac406 100644 --- a/components/input/ClearableLabeledInput.tsx +++ b/components/input/ClearableLabeledInput.tsx @@ -3,22 +3,23 @@ import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; import { getInputClassName } from './Input'; import PropTypes from '../_util/vue-types'; import { cloneElement } from '../_util/vnode'; -import { getComponent } from '../_util/props-util'; -import type { VNode } from 'vue'; -import { defineComponent } from 'vue'; +import type { PropType, VNode } from 'vue'; +import { ref, defineComponent } from 'vue'; import { tuple } from '../_util/type'; +import type { Direction, SizeType } from '../config-provider'; +import type { MouseEventHandler } from '../_util/EventInterface'; -export function hasPrefixSuffix(instance: any) { - return !!( - getComponent(instance, 'prefix') || - getComponent(instance, 'suffix') || - instance.$props.allowClear - ); +export function hasPrefixSuffix(propsAndSlots: any) { + return !!(propsAndSlots.prefix || propsAndSlots.suffix || propsAndSlots.allowClear); +} + +function hasAddon(propsAndSlots: any) { + return !!(propsAndSlots.addonBefore || propsAndSlots.addonAfter); } const ClearableInputType = ['text', 'input']; -const ClearableLabeledInput = defineComponent({ +export default defineComponent({ name: 'ClearableLabeledInput', inheritAttrs: false, props: { @@ -27,93 +28,125 @@ const ClearableLabeledInput = defineComponent({ value: PropTypes.any, defaultValue: PropTypes.any, allowClear: PropTypes.looseBool, - element: PropTypes.VNodeChild, + element: PropTypes.any, handleReset: PropTypes.func, disabled: PropTypes.looseBool, - size: PropTypes.oneOf(tuple('small', 'large', 'default')), - suffix: PropTypes.VNodeChild, - prefix: PropTypes.VNodeChild, - addonBefore: PropTypes.VNodeChild, - addonAfter: PropTypes.VNodeChild, + direction: { type: String as PropType }, + size: { type: String as PropType }, + suffix: PropTypes.any, + prefix: PropTypes.any, + addonBefore: PropTypes.any, + addonAfter: PropTypes.any, readonly: PropTypes.looseBool, - isFocused: PropTypes.looseBool, + focused: PropTypes.looseBool, + bordered: PropTypes.looseBool, + triggerFocus: { type: Function as PropType<() => void> }, }, - methods: { - renderClearIcon(prefixCls: string) { - const { allowClear, value, disabled, readonly, inputType, handleReset } = this.$props; + setup(props, { slots, attrs }) { + const containerRef = ref(); + const onInputMouseUp: MouseEventHandler = e => { + if (containerRef.value?.contains(e.target as Element)) { + const { triggerFocus } = props; + triggerFocus?.(); + } + }; + const renderClearIcon = (prefixCls: string) => { + const { allowClear, value, disabled, readonly, handleReset } = props; if (!allowClear) { return null; } - const showClearIcon = - !disabled && !readonly && value !== undefined && value !== null && value !== ''; - const className = - inputType === ClearableInputType[0] - ? `${prefixCls}-textarea-clear-icon` - : `${prefixCls}-clear-icon`; + const needClear = !disabled && !readonly && value; + const className = `${prefixCls}-clear-icon`; return ( ); - }, + }; - renderSuffix(prefixCls: string) { - const { suffix, allowClear } = this.$props; + const renderSuffix = (prefixCls: string) => { + const { suffix = slots.suffix?.(), allowClear } = props; if (suffix || allowClear) { return ( - {this.renderClearIcon(prefixCls)} + {renderClearIcon(prefixCls)} {suffix} ); } return null; - }, + }; - renderLabeledIcon(prefixCls: string, element: VNode): VNode { - const props = this.$props; - const { style } = this.$attrs; - const suffix = this.renderSuffix(prefixCls); - if (!hasPrefixSuffix(this)) { + const renderLabeledIcon = (prefixCls: string, element: VNode) => { + const { + focused, + value, + prefix = slots.prefix?.(), + size, + suffix = slots.suffix?.(), + disabled, + allowClear, + direction, + readonly, + bordered, + addonAfter = slots.addonAfter, + addonBefore = slots.addonBefore, + } = props; + const suffixNode = renderSuffix(prefixCls); + if (!hasPrefixSuffix({ prefix, suffix, allowClear })) { return cloneElement(element, { - value: props.value, + value, }); } - const prefix = props.prefix ? ( - {props.prefix} - ) : null; + const prefixNode = prefix ? {prefix} : null; - const affixWrapperCls = classNames(this.$attrs?.class, `${prefixCls}-affix-wrapper`, { - [`${prefixCls}-affix-wrapper-focused`]: props.isFocused, - [`${prefixCls}-affix-wrapper-disabled`]: props.disabled, - [`${prefixCls}-affix-wrapper-sm`]: props.size === 'small', - [`${prefixCls}-affix-wrapper-lg`]: props.size === 'large', - [`${prefixCls}-affix-wrapper-input-with-clear-btn`]: - props.suffix && props.allowClear && this.$props.value, + const affixWrapperCls = classNames(`${prefixCls}-affix-wrapper`, { + [`${prefixCls}-affix-wrapper-focused`]: focused, + [`${prefixCls}-affix-wrapper-disabled`]: disabled, + [`${prefixCls}-affix-wrapper-sm`]: size === 'small', + [`${prefixCls}-affix-wrapper-lg`]: size === 'large', + [`${prefixCls}-affix-wrapper-input-with-clear-btn`]: suffix && allowClear && value, + [`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl', + [`${prefixCls}-affix-wrapper-readonly`]: readonly, + [`${prefixCls}-affix-wrapper-borderless`]: !bordered, + // className will go to addon wrapper + [`${attrs.class}`]: !hasAddon({ addonAfter, addonBefore }) && attrs.class, }); return ( - - {prefix} + + {prefixNode} {cloneElement(element, { style: null, - value: props.value, - class: getInputClassName(prefixCls, props.size, props.disabled), + value, + class: getInputClassName(prefixCls, bordered, size, disabled), })} - {suffix} + {suffixNode} - ) as VNode; - }, + ); + }; - renderInputWithLabel(prefixCls: string, labeledElement: VNode) { - const { addonBefore, addonAfter, size } = this.$props; - const { style, class: className } = this.$attrs; + const renderInputWithLabel = (prefixCls: string, labeledElement: VNode) => { + const { + addonBefore = slots.addonBefore?.(), + addonAfter = slots.addonAfter?.(), + size, + direction, + } = props; // Not wrap when there is not addons - if (!addonBefore && !addonAfter) { + if (!hasAddon({ addonBefore, addonAfter })) { return labeledElement; } @@ -124,19 +157,24 @@ const ClearableLabeledInput = defineComponent({ ) : null; const addonAfterNode = addonAfter ? {addonAfter} : null; - const mergedWrapperClassName = classNames(`${prefixCls}-wrapper`, { - [wrapperClassName]: addonBefore || addonAfter, + const mergedWrapperClassName = classNames(`${prefixCls}-wrapper`, wrapperClassName, { + [`${wrapperClassName}-rtl`]: direction === 'rtl', }); - const mergedGroupClassName = classNames(className, `${prefixCls}-group-wrapper`, { - [`${prefixCls}-group-wrapper-sm`]: size === 'small', - [`${prefixCls}-group-wrapper-lg`]: size === 'large', - }); + const mergedGroupClassName = classNames( + `${prefixCls}-group-wrapper`, + { + [`${prefixCls}-group-wrapper-sm`]: size === 'small', + [`${prefixCls}-group-wrapper-lg`]: size === 'large', + [`${prefixCls}-group-wrapper-rtl`]: direction === 'rtl', + }, + attrs.class, + ); // Need another wrapper for changing display:table to display:inline-block // and put style prop in wrapper return ( - + {addonBeforeNode} {cloneElement(labeledElement, { style: null })} @@ -144,41 +182,49 @@ const ClearableLabeledInput = defineComponent({ ); - }, + }; - renderTextAreaWithClearIcon(prefixCls: string, element: VNode) { - const { value, allowClear } = this.$props; - const { style, class: className } = this.$attrs; + const renderTextAreaWithClearIcon = (prefixCls: string, element: VNode) => { + const { + value, + allowClear, + direction, + bordered, + addonAfter = slots.addonAfter, + addonBefore = slots.addonBefore, + } = props; if (!allowClear) { - return cloneElement(element, { value }); + return cloneElement(element, { + value, + }); } const affixWrapperCls = classNames( - className, `${prefixCls}-affix-wrapper`, `${prefixCls}-affix-wrapper-textarea-with-clear-btn`, + { + [`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl', + [`${prefixCls}-affix-wrapper-borderless`]: !bordered, + // className will go to addon wrapper + [`${attrs.class}`]: !hasAddon({ addonAfter, addonBefore }) && attrs.class, + }, ); return ( - + {cloneElement(element, { style: null, value, })} - {this.renderClearIcon(prefixCls)} + {renderClearIcon(prefixCls)} ); - }, + }; - renderClearableLabeledInput() { - const { prefixCls, inputType, element } = this.$props as any; + return () => { + const { prefixCls, inputType, element = slots.element?.() } = props; if (inputType === ClearableInputType[0]) { - return this.renderTextAreaWithClearIcon(prefixCls, element); + return renderTextAreaWithClearIcon(prefixCls, element); } - return this.renderInputWithLabel(prefixCls, this.renderLabeledIcon(prefixCls, element)); - }, - }, - render() { - return this.renderClearableLabeledInput(); + return renderInputWithLabel(prefixCls, renderLabeledIcon(prefixCls, element)); + }; }, }); - -export default ClearableLabeledInput; diff --git a/components/input/Group.tsx b/components/input/Group.tsx index a2da36d81..7e6f947d6 100644 --- a/components/input/Group.tsx +++ b/components/input/Group.tsx @@ -1,36 +1,46 @@ -import { defineComponent, inject } from 'vue'; +import type { PropType } from 'vue'; +import { computed, defineComponent } from 'vue'; import PropTypes from '../_util/vue-types'; -import { getSlot } from '../_util/props-util'; -import { defaultConfigProvider } from '../config-provider'; -import { tuple } from '../_util/type'; +import type { SizeType } from '../config-provider'; +import type { FocusEventHandler, MouseEventHandler } from '../_util/EventInterface'; +import useConfigInject from '../_util/hooks/useConfigInject'; export default defineComponent({ name: 'AInputGroup', props: { prefixCls: PropTypes.string, - size: PropTypes.oneOf(tuple('small', 'large', 'default')), + size: { type: String as PropType }, compact: PropTypes.looseBool, + onMouseenter: { type: Function as PropType }, + onMouseleave: { type: Function as PropType }, + onFocus: { type: Function as PropType }, + onBlur: { type: Function as PropType }, }, - setup() { - return { - configProvider: inject('configProvider', defaultConfigProvider), + setup(props, { slots }) { + const { prefixCls, direction } = useConfigInject('input-group', props); + const cls = computed(() => { + const pre = prefixCls.value; + return { + [`${pre}`]: true, + [`${pre}-lg`]: props.size === 'large', + [`${pre}-sm`]: props.size === 'small', + [`${pre}-compact`]: props.compact, + [`${pre}-rtl`]: direction.value === 'rtl', + }; + }); + return () => { + const {} = props; + return ( + + {slots.default?.()} + + ); }; }, - computed: { - classes() { - const { prefixCls: customizePrefixCls, size, compact = false, configProvider } = this as any; - const getPrefixCls = configProvider.getPrefixCls; - const prefixCls = getPrefixCls('input-group', customizePrefixCls); - - return { - [`${prefixCls}`]: true, - [`${prefixCls}-lg`]: size === 'large', - [`${prefixCls}-sm`]: size === 'small', - [`${prefixCls}-compact`]: compact, - }; - }, - }, - render() { - return {getSlot(this)}; - }, }); diff --git a/components/input/Input.tsx b/components/input/Input.tsx index 79ea0cb71..721b85557 100644 --- a/components/input/Input.tsx +++ b/components/input/Input.tsx @@ -1,13 +1,25 @@ import type { VNode } from 'vue'; -import { defineComponent, inject, nextTick, withDirectives } from 'vue'; +import { + getCurrentInstance, + onBeforeUnmount, + onMounted, + watch, + ref, + defineComponent, + nextTick, + withDirectives, +} from 'vue'; import antInputDirective from '../_util/antInputDirective'; import classNames from '../_util/classNames'; +import type { InputProps } from './inputProps'; import inputProps from './inputProps'; -import { hasProp, getComponent, getOptionProps } from '../_util/props-util'; -import { defaultConfigProvider } from '../config-provider'; +import type { Direction, SizeType } from '../config-provider'; import ClearableLabeledInput from './ClearableLabeledInput'; import { useInjectFormItemContext } from '../form/FormItemContext'; import omit from '../_util/omit'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import type { ChangeEvent, FocusEventHandler } from '../_util/EventInterface'; +import { controlDefaultValue } from '../_util/util'; export function fixControlledValue(value: string | number) { if (typeof value === 'undefined' || value === null) { @@ -16,142 +28,250 @@ export function fixControlledValue(value: string | number) { return value; } -export function resolveOnChange(target: HTMLInputElement, e: Event, onChange?: Function) { - if (onChange) { - const event = e as any; - if (e.type === 'click') { - // click clear icon - //event = Object.create(e); - Object.defineProperty(event, 'target', { - writable: true, - }); - Object.defineProperty(event, 'currentTarget', { - writable: true, - }); - event.target = target; - event.currentTarget = target; - const originalInputValue = target.value; - // change target ref value cause e.target.value should be '' when clear input - target.value = ''; - onChange(event); - // reset target ref value - target.value = originalInputValue; - return; - } - onChange(event); +export function resolveOnChange( + target: HTMLInputElement, + e: Event, + onChange: Function, + targetValue?: string, +) { + if (!onChange) { + return; } + const event: any = e; + const originalInputValue = target.value; + + if (e.type === 'click') { + Object.defineProperty(event, 'target', { + writable: true, + }); + Object.defineProperty(event, 'currentTarget', { + writable: true, + }); + // click clear icon + //event = Object.create(e); + event.target = target; + event.currentTarget = target; + // change target ref value cause e.target.value should be '' when clear input + target.value = ''; + onChange(event); + // reset target ref value + target.value = originalInputValue; + return; + } + // Trigger by composition event, this means we need force change the input value + if (targetValue !== undefined) { + Object.defineProperty(event, 'target', { + writable: true, + }); + Object.defineProperty(event, 'currentTarget', { + writable: true, + }); + event.target = target; + event.currentTarget = target; + target.value = targetValue; + onChange(event); + return; + } + onChange(event); } -export function getInputClassName(prefixCls: string, size: string, disabled: boolean) { +export function getInputClassName( + prefixCls: string, + bordered: boolean, + size?: SizeType, + disabled?: boolean, + direction?: Direction, +) { return classNames(prefixCls, { [`${prefixCls}-sm`]: size === 'small', [`${prefixCls}-lg`]: size === 'large', [`${prefixCls}-disabled`]: disabled, + [`${prefixCls}-rtl`]: direction === 'rtl', + [`${prefixCls}-borderless`]: !bordered, }); } +export interface InputFocusOptions extends FocusOptions { + cursor?: 'start' | 'end' | 'all'; +} +export function triggerFocus( + element?: HTMLInputElement | HTMLTextAreaElement, + option?: InputFocusOptions, +) { + if (!element) return; + + element.focus(option); + + // Selection content + const { cursor } = option || {}; + if (cursor) { + const len = element.value.length; + + switch (cursor) { + case 'start': + element.setSelectionRange(0, 0); + break; + + case 'end': + element.setSelectionRange(len, len); + break; + + default: + element.setSelectionRange(0, len); + } + } +} + export default defineComponent({ name: 'AInput', inheritAttrs: false, props: { ...inputProps, }, - setup() { + setup(props, { slots, attrs, expose, emit }) { + const inputRef = ref(); + const clearableInputRef = ref(); + let removePasswordTimeout: any; const formItemContext = useInjectFormItemContext(); - return { - configProvider: inject('configProvider', defaultConfigProvider), - removePasswordTimeout: undefined, - input: null, - clearableInput: null, - formItemContext, - }; - }, - data() { - const props = this.$props; - const value = typeof props.value === 'undefined' ? props.defaultValue : props.value; - return { - stateValue: typeof value === 'undefined' ? '' : value, - isFocused: false, - }; - }, - watch: { - value(val) { - this.stateValue = val; - }, - }, - mounted() { - nextTick(() => { - if (process.env.NODE_ENV === 'test') { - if (this.autofocus) { - this.focus(); + const { direction, prefixCls, size, autocomplete } = useConfigInject('input', props); + const stateValue = ref(props.value === controlDefaultValue ? props.defaultValue : props.value); + const focused = ref(false); + + watch( + () => props.value, + () => { + if (props.value !== controlDefaultValue) { + stateValue.value = props.value; } - } - this.clearPasswordValueAttribute(); + }, + ); + + const clearPasswordValueAttribute = () => { + // https://github.com/ant-design/ant-design/issues/20541 + removePasswordTimeout = setTimeout(() => { + if ( + inputRef.value?.getAttribute('type') === 'password' && + inputRef.value.hasAttribute('value') + ) { + inputRef.value.removeAttribute('value'); + } + }); + }; + + const focus = (option?: InputFocusOptions) => { + triggerFocus(inputRef.value, option); + }; + + const blur = () => { + inputRef.value?.blur(); + }; + + const setSelectionRange = ( + start: number, + end: number, + direction?: 'forward' | 'backward' | 'none', + ) => { + inputRef.value?.setSelectionRange(start, end, direction); + }; + + const select = () => { + inputRef.value?.select(); + }; + + expose({ + focus, + blur, + inputRef, + stateValue, + setSelectionRange, + select, }); - }, - beforeUnmount() { - if (this.removePasswordTimeout) { - clearTimeout(this.removePasswordTimeout); - } - }, - methods: { - handleInputFocus(e: Event) { - this.isFocused = true; - this.onFocus && this.onFocus(e); - }, - handleInputBlur(e: Event) { - this.isFocused = false; - this.onBlur && this.onBlur(e); - this.formItemContext.onFieldBlur(); - }, + const onFocus: FocusEventHandler = e => { + const { onFocus } = props; + focused.value = true; + onFocus?.(e); + nextTick(() => { + clearPasswordValueAttribute(); + }); + }; - focus() { - this.input.focus(); - }, + const onBlur: FocusEventHandler = e => { + const { onBlur } = props; + focused.value = false; + onBlur?.(e); + formItemContext.onFieldBlur(); + nextTick(() => { + clearPasswordValueAttribute(); + }); + }; - blur() { - this.input.blur(); - }, - select() { - this.input.select(); - }, - - saveClearableInput(input: HTMLInputElement) { - this.clearableInput = input; - }, - - saveInput(input: HTMLInputElement) { - this.input = input; - }, - - setValue(value: string | number, callback?: Function) { - if (this.stateValue === value) { + const triggerChange = (e: Event) => { + emit('update:value', (e.target as HTMLInputElement).value); + emit('change', e); + emit('input', e); + formItemContext.onFieldChange(); + }; + const instance = getCurrentInstance(); + const setValue = (value: string | number, callback?: Function) => { + if (stateValue.value === value) { return; } - if (!hasProp(this, 'value')) { - this.stateValue = value; + if (props.value === controlDefaultValue) { + stateValue.value = value; } else { - (this as any).$forceUpdate(); + instance.update(); } nextTick(() => { callback && callback(); }); - }, - triggerChange(e: Event) { - this.$emit('update:value', (e.target as HTMLInputElement).value); - this.$emit('change', e); - this.$emit('input', e); - this.formItemContext.onFieldChange(); - }, - handleReset(e: Event) { - this.setValue('', () => { - this.focus(); + }; + const handleReset = (e: MouseEvent) => { + resolveOnChange(inputRef.value, e, triggerChange); + setValue('', () => { + focus(); }); - resolveOnChange(this.input, e, this.triggerChange); - }, - renderInput(prefixCls: string, { addonBefore, addonAfter }) { - const otherProps = omit(this.$props, [ + }; + + const handleChange = (e: ChangeEvent) => { + const { value, composing, isComposing } = e.target as any; + // https://github.com/vueComponent/ant-design-vue/issues/2203 + if (((isComposing || composing) && props.lazy) || stateValue.value === value) return; + const newVal = e.target.value; + resolveOnChange(inputRef.value, e, triggerChange); + setValue(newVal, () => { + clearPasswordValueAttribute(); + }); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.keyCode === 13) { + emit('pressEnter', e); + } + emit('keydown', e); + }; + + onMounted(() => { + if (process.env.NODE_ENV === 'test') { + if (props.autofocus) { + focus(); + } + } + clearPasswordValueAttribute(); + }); + onBeforeUnmount(() => { + clearTimeout(removePasswordTimeout); + }); + + const renderInput = () => { + const { + addonBefore = slots.addonBefore, + addonAfter = slots.addonAfter, + disabled, + bordered = true, + valueModifiers = {}, + } = props; + const otherProps = omit(props as InputProps & { inputType: any; placeholder: string }, [ 'prefixCls', 'onPressEnter', 'addonBefore', @@ -159,36 +279,29 @@ export default defineComponent({ 'prefix', 'suffix', 'allowClear', + // Input elements must be either controlled or uncontrolled, + // specify either the value prop, or the defaultValue prop, but not both. 'defaultValue', - 'lazy', 'size', - 'inputPrefixCls', - 'loading', + 'inputType', + 'bordered', ]); - const { - handleKeyDown, - handleChange, - handleInputFocus, - handleInputBlur, - size, - disabled, - valueModifiers = {}, - $attrs, - } = this; - const inputProps: any = { + const inputProps = { ...otherProps, - ...$attrs, - id: otherProps.id ?? this.formItemContext.id.value, - onKeydown: handleKeyDown, - class: classNames(getInputClassName(prefixCls, size, disabled), { - [$attrs.class as string]: $attrs.class && !addonBefore && !addonAfter, - }), - ref: this.saveInput, - key: 'ant-input', - onInput: handleChange, + autocomplete: autocomplete.value, onChange: handleChange, - onFocus: handleInputFocus, - onBlur: handleInputBlur, + onInput: handleChange, + onFocus, + onBlur, + onKeydown: handleKeyDown, + class: classNames( + getInputClassName(prefixCls.value, bordered, size.value, disabled, direction.value), + { + [attrs.class as string]: attrs.class && !addonBefore && !addonAfter, + }, + ), + ref: inputRef, + key: 'ant-input', }; if (valueModifiers.lazy) { delete inputProps.onInput; @@ -198,58 +311,185 @@ export default defineComponent({ } const inputNode = ; return withDirectives(inputNode as VNode, [[antInputDirective]]); - }, - clearPasswordValueAttribute() { - // https://github.com/ant-design/ant-design/issues/20541 - this.removePasswordTimeout = setTimeout(() => { - if ( - this.input && - this.input.getAttribute && - this.input.getAttribute('type') === 'password' && - this.input.hasAttribute('value') - ) { - this.input.removeAttribute('value'); - } - }); - }, - handleChange(e: Event) { - const { value, composing, isComposing } = e.target as any; - // https://github.com/vueComponent/ant-design-vue/issues/2203 - if (((isComposing || composing) && this.lazy) || this.stateValue === value) return; - this.setValue(value, this.clearPasswordValueAttribute); - resolveOnChange(this.input, e, this.triggerChange); - }, - handleKeyDown(e: KeyboardEvent) { - if (e.keyCode === 13) { - this.$emit('pressEnter', e); - } - this.$emit('keydown', e); - }, - }, - render() { - const { prefixCls: customizePrefixCls } = this.$props; - const { stateValue, isFocused } = this.$data; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('input', customizePrefixCls); - const addonAfter = getComponent(this, 'addonAfter'); - const addonBefore = getComponent(this, 'addonBefore'); - const suffix = getComponent(this, 'suffix'); - const prefix = getComponent(this, 'prefix'); - const props: any = { - ...this.$attrs, - ...getOptionProps(this), - prefixCls, - inputType: 'input', - value: fixControlledValue(stateValue), - element: this.renderInput(prefixCls, { addonAfter, addonBefore }), - handleReset: this.handleReset, - addonAfter, - addonBefore, - suffix, - prefix, - isFocused, }; - return ; + return () => { + const inputProps: any = { + ...attrs, + ...props, + prefixCls: prefixCls.value, + inputType: 'input', + value: fixControlledValue(stateValue.value), + handleReset, + focused: focused.value, + }; + + return ( + + ); + }; }, + + // methods: { + // handleInputFocus(e: Event) { + // this.isFocused = true; + // this.onFocus && this.onFocus(e); + // }, + + // handleInputBlur(e: Event) { + // this.isFocused = false; + // this.onBlur && this.onBlur(e); + // this.formItemContext.onFieldBlur(); + // }, + + // focus() { + // this.input.focus(); + // }, + + // blur() { + // this.input.blur(); + // }, + // select() { + // this.input.select(); + // }, + + // saveClearableInput(input: HTMLInputElement) { + // this.clearableInput = input; + // }, + + // saveInput(input: HTMLInputElement) { + // this.input = input; + // }, + + // setValue(value: string | number, callback?: Function) { + // if (this.stateValue === value) { + // return; + // } + // if (!hasProp(this, 'value')) { + // this.stateValue = value; + // } else { + // (this as any).$forceUpdate(); + // } + // nextTick(() => { + // callback && callback(); + // }); + // }, + // triggerChange(e: Event) { + // this.$emit('update:value', (e.target as HTMLInputElement).value); + // this.$emit('change', e); + // this.$emit('input', e); + // this.formItemContext.onFieldChange(); + // }, + // handleReset(e: Event) { + // this.setValue('', () => { + // this.focus(); + // }); + // resolveOnChange(this.input, e, this.triggerChange); + // }, + // renderInput(prefixCls: string, { addonBefore, addonAfter }) { + // const otherProps = omit(this.$props, [ + // 'prefixCls', + // 'onPressEnter', + // 'addonBefore', + // 'addonAfter', + // 'prefix', + // 'suffix', + // 'allowClear', + // 'defaultValue', + // 'lazy', + // 'size', + // 'inputPrefixCls', + // 'loading', + // ]); + // const { + // handleKeyDown, + // handleChange, + // handleInputFocus, + // handleInputBlur, + // size, + // disabled, + // valueModifiers = {}, + // $attrs, + // } = this; + // const inputProps: any = { + // ...otherProps, + // ...$attrs, + // id: otherProps.id ?? this.formItemContext.id.value, + // onKeydown: handleKeyDown, + // class: classNames(getInputClassName(prefixCls, size, disabled), { + // [$attrs.class as string]: $attrs.class && !addonBefore && !addonAfter, + // }), + // ref: this.saveInput, + // key: 'ant-input', + // onInput: handleChange, + // onChange: handleChange, + // onFocus: handleInputFocus, + // onBlur: handleInputBlur, + // }; + // if (valueModifiers.lazy) { + // delete inputProps.onInput; + // } + // if (!inputProps.autofocus) { + // delete inputProps.autofocus; + // } + // const inputNode = ; + // return withDirectives(inputNode as VNode, [[antInputDirective]]); + // }, + // clearPasswordValueAttribute() { + // // https://github.com/ant-design/ant-design/issues/20541 + // this.removePasswordTimeout = setTimeout(() => { + // if ( + // this.input && + // this.input.getAttribute && + // this.input.getAttribute('type') === 'password' && + // this.input.hasAttribute('value') + // ) { + // this.input.removeAttribute('value'); + // } + // }); + // }, + // handleChange(e: Event) { + // const { value, composing, isComposing } = e.target as any; + // // https://github.com/vueComponent/ant-design-vue/issues/2203 + // if (((isComposing || composing) && this.lazy) || this.stateValue === value) return; + // this.setValue(value, this.clearPasswordValueAttribute); + // resolveOnChange(this.input, e, this.triggerChange); + // }, + // handleKeyDown(e: KeyboardEvent) { + // if (e.keyCode === 13) { + // this.$emit('pressEnter', e); + // } + // this.$emit('keydown', e); + // }, + // }, + // render() { + // const { prefixCls: customizePrefixCls } = this.$props; + // const { stateValue, isFocused } = this.$data; + // const getPrefixCls = this.configProvider.getPrefixCls; + // const prefixCls = getPrefixCls('input', customizePrefixCls); + // const addonAfter = getComponent(this, 'addonAfter'); + // const addonBefore = getComponent(this, 'addonBefore'); + // const suffix = getComponent(this, 'suffix'); + // const prefix = getComponent(this, 'prefix'); + // const props: any = { + // ...this.$attrs, + // ...getOptionProps(this), + // prefixCls, + // inputType: 'input', + // value: fixControlledValue(stateValue), + // element: this.renderInput(prefixCls, { addonAfter, addonBefore }), + // handleReset: this.handleReset, + // addonAfter, + // addonBefore, + // suffix, + // prefix, + // isFocused, + // }; + + // return ; + // }, }); diff --git a/components/input/Password.tsx b/components/input/Password.tsx index 1f0560956..ab87cf052 100644 --- a/components/input/Password.tsx +++ b/components/input/Password.tsx @@ -1,20 +1,23 @@ import classNames from '../_util/classNames'; -import { getComponent, getOptionProps } from '../_util/props-util'; +import { isValidElement } from '../_util/props-util'; import { cloneElement } from '../_util/vnode'; import Input from './Input'; import EyeOutlined from '@ant-design/icons-vue/EyeOutlined'; import EyeInvisibleOutlined from '@ant-design/icons-vue/EyeInvisibleOutlined'; +import type { InputProps } from './inputProps'; import inputProps from './inputProps'; import PropTypes from '../_util/vue-types'; import BaseMixin from '../_util/BaseMixin'; -import { defineComponent, inject } from 'vue'; -import { defaultConfigProvider } from '../config-provider'; +import { computed, defineComponent, ref } from 'vue'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import omit from '../_util/omit'; const ActionMap = { click: 'onClick', hover: 'onMouseover', }; - +const defaultIconRender = (visible: boolean) => + visible ? : ; export default defineComponent({ name: 'AInputPassword', mixins: [BaseMixin], @@ -25,96 +28,76 @@ export default defineComponent({ inputPrefixCls: PropTypes.string, action: PropTypes.string.def('click'), visibilityToggle: PropTypes.looseBool.def(true), - iconRender: PropTypes.func.def((visible: boolean) => - visible ? : , - ), + iconRender: PropTypes.func, }, - setup() { - return { - input: null, - configProvider: inject('configProvider', defaultConfigProvider), - }; - }, - data() { - return { - visible: false, - }; - }, - methods: { - saveInput(node: any) { - this.input = node; - }, - focus() { - this.input.focus(); - }, - blur() { - this.input.blur(); - }, - onVisibleChange() { - if (this.disabled) { + setup(props, { slots, attrs, expose }) { + const visible = ref(false); + const onVisibleChange = () => { + const { disabled } = props; + if (disabled) { return; } - this.setState({ - visible: !this.visible, - }); - }, - getIcon(prefixCls) { - const { action } = this.$props; - const iconTrigger = ActionMap[action] || ''; - const iconRender = this.$slots.iconRender || this.$props.iconRender; - const icon = iconRender(this.visible); + visible.value = !visible.value; + }; + const inputRef = ref(); + const focus = () => { + inputRef.value?.focus(); + }; + const blur = () => { + inputRef.value?.blur(); + }; + expose({ + focus, + blur, + }); + const getIcon = (prefixCls: string) => { + const { action, iconRender = slots.iconRender || defaultIconRender } = props; + const iconTrigger = ActionMap[action!] || ''; + const icon = iconRender(visible.value); const iconProps = { - [iconTrigger]: this.onVisibleChange, - onMousedown: (e: Event) => { + [iconTrigger]: onVisibleChange, + class: `${prefixCls}-icon`, + key: 'passwordIcon', + onMousedown: (e: MouseEvent) => { // Prevent focused state lost // https://github.com/ant-design/ant-design/issues/15173 e.preventDefault(); }, - onMouseup: (e: Event) => { - // Prevent focused state lost - // https://github.com/ant-design/ant-design/pull/23633/files + onMouseup: (e: MouseEvent) => { + // Prevent caret position change + // https://github.com/ant-design/ant-design/issues/23524 e.preventDefault(); }, - class: `${prefixCls}-icon`, - key: 'passwordIcon', }; - return cloneElement(icon, iconProps); - }, - }, - render() { - const { - prefixCls: customizePrefixCls, - inputPrefixCls: customizeInputPrefixCls, - size, - suffix, - action, - visibilityToggle, - iconRender, - ...restProps - } = getOptionProps(this); - const { class: className } = this.$attrs; - - const getPrefixCls = this.configProvider.getPrefixCls; - const inputPrefixCls = getPrefixCls('input', customizeInputPrefixCls); - const prefixCls = getPrefixCls('input-password', customizePrefixCls); - - const suffixIcon = visibilityToggle && this.getIcon(prefixCls); - const inputClassName = classNames(prefixCls, className, { - [`${prefixCls}-${size}`]: !!size, - }); - const inputProps = { - ...restProps, - prefixCls: inputPrefixCls, - size, - suffix: suffixIcon, - prefix: getComponent(this, 'prefix'), - addonAfter: getComponent(this, 'addonAfter'), - addonBefore: getComponent(this, 'addonBefore'), - ...this.$attrs, - type: this.visible ? 'text' : 'password', - class: inputClassName, - ref: 'input', + return cloneElement(isValidElement(icon) ? icon : {icon}, iconProps); + }; + const { prefixCls, getPrefixCls } = useConfigInject('input-password', props); + const inputPrefixCls = computed(() => getPrefixCls('input', props.inputPrefixCls)); + const renderPassword = () => { + const { size, visibilityToggle, ...restProps } = props; + + const suffixIcon = visibilityToggle && getIcon(prefixCls.value); + const inputClassName = classNames(prefixCls.value, attrs.class, { + [`${prefixCls.value}-${size}`]: !!size, + }); + + const omittedProps = { + ...omit(restProps, ['suffix', 'iconRender']), + ...attrs, + type: visible.value ? 'text' : 'password', + class: inputClassName, + prefixCls: inputPrefixCls.value, + suffix: suffixIcon, + } as InputProps; + + if (size) { + omittedProps.size = size; + } + + return ; + }; + return () => { + return renderPassword(); }; - return ; }, }); diff --git a/components/input/Search.tsx b/components/input/Search.tsx index 77388c695..d2708453a 100644 --- a/components/input/Search.tsx +++ b/components/input/Search.tsx @@ -1,187 +1,145 @@ -import { defineComponent, inject } from 'vue'; +import type { PropType } from 'vue'; +import { computed, ref, defineComponent } from 'vue'; import classNames from '../_util/classNames'; -import isMobile from '../_util/isMobile'; import Input from './Input'; -import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; import SearchOutlined from '@ant-design/icons-vue/SearchOutlined'; import inputProps from './inputProps'; import Button from '../button'; import { cloneElement } from '../_util/vnode'; import PropTypes from '../_util/vue-types'; -import { getOptionProps, getComponent } from '../_util/props-util'; -import { defaultConfigProvider } from '../config-provider'; import isPlainObject from 'lodash-es/isPlainObject'; +import type { ChangeEvent, MouseEventHandler } from '../_util/EventInterface'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import omit from '../_util/omit'; +import isMobile from '../_util/isMobile'; export default defineComponent({ name: 'AInputSearch', inheritAttrs: false, props: { ...inputProps, + inputPrefixCls: PropTypes.string, // 不能设置默认值 https://github.com/vueComponent/ant-design-vue/issues/1916 - enterButton: PropTypes.VNodeChild, - onSearch: PropTypes.func, + enterButton: PropTypes.any, + onSearch: { + type: Function as PropType< + (value: string, event?: ChangeEvent | MouseEvent | KeyboardEvent) => void + >, + }, }, - setup() { - return { - configProvider: inject('configProvider', defaultConfigProvider), - input: null, + setup(props, { slots, attrs, expose, emit }) { + const inputRef = ref(); + const focus = () => { + inputRef.value?.focus(); }; - }, - methods: { - saveInput(node: HTMLInputElement) { - this.input = node; - }, - handleChange(e: Event) { - this.$emit('update:value', (e.target as HTMLInputElement).value); + const blur = () => { + inputRef.value?.blur(); + }; + expose({ + focus, + blur, + }); + + const onChange = (e: ChangeEvent) => { + emit('update:value', (e.target as HTMLInputElement).value); if (e && e.target && e.type === 'click') { - this.$emit('search', (e.target as HTMLInputElement).value, e); + emit('search', e.target.value, e); } - this.$emit('change', e); - }, - handleSearch(e: Event) { - if (this.loading || this.disabled) { - return; + emit('change', e); + }; + + const onMousedown: MouseEventHandler = e => { + if (document.activeElement === inputRef.value?.inputRef.value) { + e.preventDefault(); } - this.$emit('search', this.input.stateValue, e); + }; + + const onSearch = (e: MouseEvent | KeyboardEvent) => { + emit('search', inputRef.value?.stateValue, e); if (!isMobile.tablet) { - this.input.focus(); + inputRef.value.focus(); } - }, - focus() { - this.input.focus(); - }, + }; - blur() { - this.input.blur(); - }, - renderLoading(prefixCls: string) { - const { size } = this.$props; - let enterButton = getComponent(this, 'enterButton'); - // 兼容 , 因enterButton类型为 any,此类写法 enterButton 为空字符串 + const { prefixCls, getPrefixCls, direction, size } = useConfigInject('input-search', props); + const inputPrefixCls = computed(() => getPrefixCls('input', props.inputPrefixCls)); + return () => { + const { + disabled, + loading, + addonAfter = slots.addonAfter?.(), + suffix = slots.suffix?.(), + ...restProps + } = props; + let { enterButton = slots.enterButton?.() } = props; enterButton = enterButton || enterButton === ''; - if (enterButton) { - return ( - - ); - } - return ; - }, - renderSuffix(prefixCls: string) { - const { loading } = this; - const suffix = getComponent(this, 'suffix'); - let enterButton = getComponent(this, 'enterButton'); - // 兼容 , 因enterButton类型为 any,此类写法 enterButton 为空字符串 - enterButton = enterButton || enterButton === ''; - if (loading && !enterButton) { - return [suffix, this.renderLoading(prefixCls)]; - } + const searchIcon = typeof enterButton === 'boolean' ? : null; + const btnClassName = `${prefixCls.value}-button`; - if (enterButton) return suffix; - - const icon = ( - - ); - - if (suffix) { - // let cloneSuffix = suffix; - // if (isValidElement(cloneSuffix) && !cloneSuffix.key) { - // cloneSuffix = cloneElement(cloneSuffix, { - // key: 'originSuffix', - // }); - // } - return [suffix, icon]; - } - - return icon; - }, - renderAddonAfter(prefixCls: string) { - const { size, disabled, loading } = this; - const btnClassName = `${prefixCls}-button`; - let enterButton = getComponent(this, 'enterButton'); - enterButton = enterButton || enterButton === ''; - const addonAfter = getComponent(this, 'addonAfter'); - if (loading && enterButton) { - return [this.renderLoading(prefixCls), addonAfter]; - } - if (!enterButton) return addonAfter; const enterButtonAsElement = Array.isArray(enterButton) ? enterButton[0] : enterButton; let button: any; const isAntdButton = enterButtonAsElement.type && isPlainObject(enterButtonAsElement.type) && enterButtonAsElement.type.__ANT_BUTTON; - if (enterButtonAsElement.tagName === 'button' || isAntdButton) { + + if (isAntdButton || enterButtonAsElement.tagName === 'button') { button = cloneElement(enterButtonAsElement, { + onMousedown, + onClick: onSearch, key: 'enterButton', - class: isAntdButton ? btnClassName : '', - ...(isAntdButton ? { size } : {}), - onClick: this.handleSearch, + ...(isAntdButton + ? { + class: btnClassName, + size: size.value, + } + : {}), }); } else { button = ( ); } if (addonAfter) { - return [button, addonAfter]; + button = [button, addonAfter]; } - - return button; - }, - }, - render() { - const { - prefixCls: customizePrefixCls, - inputPrefixCls: customizeInputPrefixCls, - size, - class: className, - ...restProps - } = { ...getOptionProps(this), ...this.$attrs } as any; - delete restProps.onSearch; - delete restProps.loading; - delete restProps.enterButton; - delete restProps.addonBefore; - delete restProps['onUpdate:value']; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('input-search', customizePrefixCls); - const inputPrefixCls = getPrefixCls('input', customizeInputPrefixCls); - - let enterButton = getComponent(this, 'enterButton'); - const addonBefore = getComponent(this, 'addonBefore'); - enterButton = enterButton || enterButton === ''; - let inputClassName: string; - if (enterButton) { - inputClassName = classNames(prefixCls, className, { - [`${prefixCls}-enter-button`]: !!enterButton, - [`${prefixCls}-${size}`]: !!size, - }); - } else { - inputClassName = classNames(prefixCls, className); - } - - const inputProps = { - ...restProps, - prefixCls: inputPrefixCls, - size, - suffix: this.renderSuffix(prefixCls), - prefix: getComponent(this, 'prefix'), - addonAfter: this.renderAddonAfter(prefixCls), - addonBefore, - class: inputClassName, - onPressEnter: this.handleSearch, - onChange: this.handleChange, + const cls = classNames( + prefixCls.value, + { + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + [`${prefixCls.value}-${size.value}`]: !!size.value, + [`${prefixCls.value}-with-button`]: !!enterButton, + }, + attrs.class, + ); + return ( + + ); }; - return ; }, }); diff --git a/components/input/inputProps.ts b/components/input/inputProps.ts index dadeebddb..7d15830a2 100644 --- a/components/input/inputProps.ts +++ b/components/input/inputProps.ts @@ -1,29 +1,62 @@ -import type { PropType } from 'vue'; +import type { ExtractPropTypes, PropType } from 'vue'; import PropTypes from '../_util/vue-types'; import type { SizeType } from '../config-provider'; -export default { +import { controlDefaultValue } from '../_util/util'; +export const inputDefaultValue = Symbol() as unknown as string; +const inputProps = { id: PropTypes.string, prefixCls: PropTypes.string, inputPrefixCls: PropTypes.string, defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + value: { + type: [String, Number, Symbol] as PropType, + default: controlDefaultValue, + }, placeholder: { type: [String, Number] as PropType, }, - type: PropTypes.string.def('text'), + autocomplete: String, + type: { + type: String as PropType< + | 'button' + | 'checkbox' + | 'color' + | 'date' + | 'datetime-local' + | 'email' + | 'file' + | 'hidden' + | 'image' + | 'month' + | 'number' + | 'password' + | 'radio' + | 'range' + | 'reset' + | 'search' + | 'submit' + | 'tel' + | 'text' + | 'time' + | 'url' + | 'week' + >, + default: 'text', + }, name: PropTypes.string, size: { type: String as PropType }, disabled: PropTypes.looseBool, readonly: PropTypes.looseBool, - addonBefore: PropTypes.VNodeChild, - addonAfter: PropTypes.VNodeChild, - prefix: PropTypes.VNodeChild, - suffix: PropTypes.VNodeChild, + addonBefore: PropTypes.any, + addonAfter: PropTypes.any, + prefix: PropTypes.any, + suffix: PropTypes.any, autofocus: PropTypes.looseBool, allowClear: PropTypes.looseBool, lazy: PropTypes.looseBool.def(true), maxlength: PropTypes.number, loading: PropTypes.looseBool, + bordered: PropTypes.looseBool, onPressEnter: PropTypes.func, onKeydown: PropTypes.func, onKeyup: PropTypes.func, @@ -34,3 +67,5 @@ export default { 'onUpdate:value': PropTypes.func, valueModifiers: Object, }; +export default inputProps; +export type InputProps = Partial>; diff --git a/components/input/style/search-input.less b/components/input/style/search-input.less index 384f14af8..06d52022f 100644 --- a/components/input/style/search-input.less +++ b/components/input/style/search-input.less @@ -6,29 +6,66 @@ @search-prefix: ~'@{ant-prefix}-input-search'; .@{search-prefix} { - &-icon { - color: @text-color-secondary; - cursor: pointer; - transition: all 0.3s; - &:hover { - color: @input-icon-hover-color; + .@{ant-prefix}-input { + &:hover, + &:focus { + border-color: @input-hover-border-color; + + + .@{ant-prefix}-input-group-addon .@{search-prefix}-button:not(.@{ant-prefix}-btn-primary) { + border-left-color: @input-hover-border-color; + } } } - &-enter-button { - input { - border-right: 0; - } + .@{ant-prefix}-input-affix-wrapper { + border-radius: 0; + } - & + .@{ant-prefix}-input-group-addon, - input + .@{ant-prefix}-input-group-addon { + // fix slight height diff in Firefox: + // https://ant.design/components/auto-complete-cn/#components-auto-complete-demo-certain-category + .@{ant-prefix}-input-lg { + line-height: @line-height-base - 0.0002; + } + + > .@{ant-prefix}-input-group { + > .@{ant-prefix}-input-group-addon:last-child { + left: -1px; padding: 0; border: 0; .@{search-prefix}-button { - border-top-left-radius: 0; - border-bottom-left-radius: 0; + padding-top: 0; + padding-bottom: 0; + border-radius: 0 @border-radius-base @border-radius-base 0; + } + + .@{search-prefix}-button:not(.@{ant-prefix}-btn-primary) { + color: @text-color-secondary; + + &.@{ant-prefix}-btn-loading::before { + top: 0; + right: 0; + bottom: 0; + left: 0; + } } } } + + &-button { + height: @input-height-base; + + &:hover, + &:focus { + z-index: 1; + } + } + + &-large &-button { + height: @input-height-lg; + } + + &-small &-button { + height: @input-height-sm; + } }