diff --git a/build/config.js b/build/config.js index 2d7f6e9dc..a75a866b4 100644 --- a/build/config.js +++ b/build/config.js @@ -1,5 +1,5 @@ module.exports = { dev: { - componentName: 'slider', // dev components + componentName: 'input', // dev components }, }; diff --git a/components/affix/index.jsx b/components/affix/index.jsx index 7dd2672c2..d67679fc9 100644 --- a/components/affix/index.jsx +++ b/components/affix/index.jsx @@ -1,9 +1,8 @@ import PropTypes from '../_util/vue-types'; -import addEventListener from '../vc-util/Dom/addEventListener'; import classNames from 'classnames'; import shallowequal from 'shallowequal'; import omit from 'omit.js'; -import getScroll from '../_util/getScroll'; +import ResizeObserver from '../vc-resize-observer'; import BaseMixin from '../_util/BaseMixin'; import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'; import { ConfigConsumerProps } from '../config-provider'; @@ -242,11 +241,17 @@ const Affix = { attrs: omit($props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target']), }; return ( -
-
- {$slots.default} + { + this.updatePosition(); + }} + > +
+
+ {$slots.default} +
-
+ ); }, }; diff --git a/components/input/ClearableLabeledInput.jsx b/components/input/ClearableLabeledInput.jsx new file mode 100644 index 000000000..9237f182b --- /dev/null +++ b/components/input/ClearableLabeledInput.jsx @@ -0,0 +1,173 @@ +import classNames from 'classnames'; +import Icon from '../icon'; +import { getInputClassName } from './Input'; +import PropTypes from '../_util/vue-types'; +import { cloneElement } from '../_util/vnode'; +import { getComponentFromProp } from '../_util/props-util'; + +export function hasPrefixSuffix(instance) { + return !!( + getComponentFromProp(instance, 'prefix') || + getComponentFromProp(instance, 'suffix') || + instance.$props.allowClear + ); +} + +const ClearableInputType = ['text', 'input']; + +const ClearableLabeledInput = { + props: { + prefixCls: PropTypes.string, + inputType: PropTypes.oneOf(ClearableInputType), + value: PropTypes.any, + defaultValue: PropTypes.any, + allowClear: PropTypes.bool, + element: PropTypes.any, + handleReset: PropTypes.func, + disabled: PropTypes.bool, + size: PropTypes.oneOf(['small', 'large', 'default']), + suffix: PropTypes.any, + prefix: PropTypes.any, + addonBefore: PropTypes.any, + addonAfter: PropTypes.any, + className: PropTypes.string, + }, + methods: { + renderClearIcon(prefixCls) { + const { allowClear, value, disabled, inputType, handleReset } = this.$props; + if (!allowClear || disabled || value === undefined || value === null || value === '') { + return null; + } + const className = + inputType === ClearableInputType[0] + ? `${prefixCls}-textarea-clear-icon` + : `${prefixCls}-clear-icon`; + return ( + + ); + }, + + renderSuffix(prefixCls) { + const { suffix, allowClear } = this.$props; + if (suffix || allowClear) { + return ( + + {this.renderClearIcon(prefixCls)} + {suffix} + + ); + } + return null; + }, + + renderLabeledIcon(prefixCls, element) { + const props = this.$props; + const suffix = this.renderSuffix(prefixCls); + if (!hasPrefixSuffix(this)) { + return cloneElement(element, { + props: { value: props.value }, + }); + } + + const prefix = props.prefix ? ( + {props.prefix} + ) : null; + + const affixWrapperCls = classNames(props.className, `${prefixCls}-affix-wrapper`, { + [`${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, + }); + return ( + + {prefix} + {cloneElement(element, { + style: null, + props: { value: props.value }, + class: getInputClassName(prefixCls, props.size, props.disabled), + })} + {suffix} + + ); + }, + + renderInputWithLabel(prefixCls, labeledElement) { + const { addonBefore, addonAfter, style, size, className } = this.$props; + // Not wrap when there is not addons + if (!addonBefore && !addonAfter) { + return labeledElement; + } + + const wrapperClassName = `${prefixCls}-group`; + const addonClassName = `${wrapperClassName}-addon`; + const addonBeforeNode = addonBefore ? ( + {addonBefore} + ) : null; + const addonAfterNode = addonAfter ? {addonAfter} : null; + + const mergedWrapperClassName = classNames(`${prefixCls}-wrapper`, { + [wrapperClassName]: addonBefore || addonAfter, + }); + + const mergedGroupClassName = classNames(className, `${prefixCls}-group-wrapper`, { + [`${prefixCls}-group-wrapper-sm`]: size === 'small', + [`${prefixCls}-group-wrapper-lg`]: size === 'large', + }); + + // Need another wrapper for changing display:table to display:inline-block + // and put style prop in wrapper + return ( + + + {addonBeforeNode} + {cloneElement(labeledElement, { style: null })} + {addonAfterNode} + + + ); + }, + + renderTextAreaWithClearIcon(prefixCls, element) { + const { value, allowClear, className, style } = this.$props; + if (!allowClear) { + return cloneElement(element, { + props: { value }, + }); + } + const affixWrapperCls = classNames( + className, + `${prefixCls}-affix-wrapper`, + `${prefixCls}-affix-wrapper-textarea-with-clear-btn`, + ); + return ( + + {cloneElement(element, { + style: null, + props: { value }, + })} + {this.renderClearIcon(prefixCls)} + + ); + }, + + renderClearableLabeledInput() { + const { prefixCls, inputType, element } = this.$props; + if (inputType === ClearableInputType[0]) { + return this.renderTextAreaWithClearIcon(prefixCls, element); + } + return this.renderInputWithLabel(prefixCls, this.renderLabeledIcon(prefixCls, element)); + }, + }, + render() { + return this.renderClearableLabeledInput(); + }, +}; + +export default ClearableLabeledInput; diff --git a/components/input/Input.jsx b/components/input/Input.jsx index 1e3a352eb..33aed8ec9 100644 --- a/components/input/Input.jsx +++ b/components/input/Input.jsx @@ -2,25 +2,45 @@ import classNames from 'classnames'; import TextArea from './TextArea'; import omit from 'omit.js'; import inputProps from './inputProps'; -import { hasProp, getComponentFromProp, getListeners } from '../_util/props-util'; +import { hasProp, getComponentFromProp, getListeners, getOptionProps } from '../_util/props-util'; import { ConfigConsumerProps } from '../config-provider'; -import Icon from '../icon'; +import ClearableLabeledInput from './ClearableLabeledInput'; function noop() {} -function fixControlledValue(value) { +export function fixControlledValue(value) { if (typeof value === 'undefined' || value === null) { return ''; } return value; } -function hasPrefixSuffix(instance) { - return !!( - getComponentFromProp(instance, 'prefix') || - getComponentFromProp(instance, 'suffix') || - instance.$props.allowClear - ); +export function resolveOnChange(target, e, onChange) { + if (onChange) { + let event = e; + if (e.type === 'click') { + // click clear icon + event = { ...e }; + 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 getInputClassName(prefixCls, size, disabled) { + return classNames(prefixCls, { + [`${prefixCls}-sm`]: size === 'small', + [`${prefixCls}-lg`]: size === 'large', + [`${prefixCls}-disabled`]: disabled, + }); } export default { @@ -37,9 +57,10 @@ export default { configProvider: { default: () => ConfigConsumerProps }, }, data() { - const { value = '', defaultValue = '' } = this.$props; + const props = this.$props; + const value = typeof props.value === 'undefined' ? props.defaultValue : props.value; return { - stateValue: !hasProp(this, 'value') ? defaultValue : value, + stateValue: value, }; }, watch: { @@ -52,16 +73,15 @@ export default { if (this.autoFocus) { this.focus(); } + this.clearPasswordValueAttribute(); }); }, + beforeDestroy() { + if (this.removePasswordTimeout) { + clearTimeout(this.removePasswordTimeout); + } + }, methods: { - handleKeyDown(e) { - if (e.keyCode === 13) { - this.$emit('pressEnter', e); - } - this.$emit('keydown', e); - }, - focus() { this.$refs.input.focus(); }, @@ -73,155 +93,30 @@ export default { this.$refs.input.select(); }, - getInputClassName(prefixCls) { - const { size, disabled } = this.$props; - return { - [`${prefixCls}`]: true, - [`${prefixCls}-sm`]: size === 'small', - [`${prefixCls}-lg`]: size === 'large', - [`${prefixCls}-disabled`]: disabled, - }; - }, - - setValue(value, e) { + setValue(value, callback) { if (this.stateValue === value) { return; } if (!hasProp(this, 'value')) { this.stateValue = value; + this.$nextTick(() => { + callback && callback(); + }); } else { this.$forceUpdate(); } - this.$emit('change.value', value); - let event = e; - if (e.type === 'click' && this.$refs.input) { - // click clear icon - event = { ...e }; - event.target = this.$refs.input; - event.currentTarget = this.$refs.input; - const originalInputValue = this.$refs.input.value; - // change input value cause e.target.value should be '' when clear input - this.$refs.input.value = ''; - this.$emit('change', event); - this.$emit('input', event); - // reset input value - this.$refs.input.value = originalInputValue; - return; - } + }, + onChange(e) { + this.$emit('change.value', e.target.value); this.$emit('change', e); this.$emit('input', e); }, - handleReset(e) { - this.setValue('', e); - this.$nextTick(() => { + this.setValue('', () => { this.focus(); }); + resolveOnChange(this.$refs.input, e, this.onChange); }, - - handleChange(e) { - const { value, composing } = e.target; - if (composing && this.lazy) return; - this.setValue(value, e); - }, - - renderClearIcon(prefixCls) { - const { allowClear, disabled } = this.$props; - const { stateValue } = this; - if ( - !allowClear || - disabled || - stateValue === undefined || - stateValue === null || - stateValue === '' - ) { - return null; - } - return ( - - ); - }, - - renderSuffix(prefixCls) { - const { allowClear } = this.$props; - let suffix = getComponentFromProp(this, 'suffix'); - if (suffix || allowClear) { - return ( - - {this.renderClearIcon(prefixCls)} - {suffix} - - ); - } - return null; - }, - - renderLabeledInput(prefixCls, children) { - const props = this.$props; - let addonAfter = getComponentFromProp(this, 'addonAfter'); - let addonBefore = getComponentFromProp(this, 'addonBefore'); - // Not wrap when there is not addons - if (!addonBefore && !addonAfter) { - return children; - } - - const wrapperClassName = `${prefixCls}-group`; - const addonClassName = `${wrapperClassName}-addon`; - addonBefore = addonBefore ? {addonBefore} : null; - - addonAfter = addonAfter ? {addonAfter} : null; - - const mergedWrapperClassName = { - [`${prefixCls}-wrapper`]: true, - [wrapperClassName]: addonBefore || addonAfter, - }; - - const mergedGroupClassName = classNames(`${prefixCls}-group-wrapper`, { - [`${prefixCls}-group-wrapper-sm`]: props.size === 'small', - [`${prefixCls}-group-wrapper-lg`]: props.size === 'large', - }); - return ( - - - {addonBefore} - {children} - {addonAfter} - - - ); - }, - renderLabeledIcon(prefixCls, children) { - const { size } = this.$props; - let suffix = this.renderSuffix(prefixCls); - if (!hasPrefixSuffix(this)) { - return children; - } - let prefix = getComponentFromProp(this, 'prefix'); - prefix = prefix ? ( - - {prefix} - - ) : null; - - const affixWrapperCls = classNames(`${prefixCls}-affix-wrapper`, { - [`${prefixCls}-affix-wrapper-sm`]: size === 'small', - [`${prefixCls}-affix-wrapper-lg`]: size === 'large', - }); - return ( - - {prefix} - {children} - {suffix} - - ); - }, - renderInput(prefixCls) { const otherProps = omit(this.$props, [ 'prefixCls', @@ -233,8 +128,10 @@ export default { 'value', 'defaultValue', 'lazy', + 'size', + 'inputType', ]); - const { stateValue, getInputClassName, handleKeyDown, handleChange } = this; + const { stateValue, handleKeyDown, handleChange, size, disabled } = this; const inputProps = { directives: [{ name: 'ant-input' }], domProps: { @@ -247,11 +144,35 @@ export default { input: handleChange, change: noop, }, - class: getInputClassName(prefixCls), + class: getInputClassName(prefixCls, size, disabled), ref: 'input', key: 'ant-input', }; - return this.renderLabeledIcon(prefixCls, ); + return ; + }, + clearPasswordValueAttribute() { + // https://github.com/ant-design/ant-design/issues/20541 + this.removePasswordTimeout = setTimeout(() => { + if ( + this.$refs.input && + this.$refs.input.getAttribute('type') === 'password' && + this.$refs.input.hasAttribute('value') + ) { + this.$refs.input.removeAttribute('value'); + } + }); + }, + handleChange(e) { + const { value, composing } = e.target; + if (composing && this.lazy) return; + this.setValue(value, this.clearPasswordValueAttribute); + resolveOnChange(this.$refs.input, e, this.onChange); + }, + handleKeyDown(e) { + if (e.keyCode === 13) { + this.$emit('pressEnter', e); + } + this.$emit('keydown', e); }, }, render() { @@ -274,8 +195,28 @@ export default { return
`; exports[`renders ./components/input/demo/autosize-textarea.md correctly 1`] = ` -
-
+
+
+
`; exports[`renders ./components/input/demo/basic.md correctly 1`] = ``; exports[`renders ./components/input/demo/group.md correctly 1`] = ` -


Zhejiang
+


Zhejiang

Option1


Option1-1
@@ -32,7 +33,7 @@ exports[`renders ./components/input/demo/group.md correctly 1`] = `

Between
-

Sign Up
+

Sign Up