Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(input): support clearTrigger #1481

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 59 additions & 22 deletions src/input/__test__/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('Input.vue', async () => {
const wrapper = mount(<Input label="标题" v-model={value.value} clearable onClear={handleClear} />);
const closeIcon = wrapper.findComponent(CloseCircleFilledIcon);
expect(closeIcon.exists()).toBeTruthy();
await closeIcon.trigger('click');
await closeIcon.trigger('mousedown');
expect(value.value).toBe('');
expect(handleClear).toBeCalled();
});
Expand Down Expand Up @@ -140,31 +140,41 @@ describe('Input.vue', async () => {
expect(attrDom1.attributes('type')).toBe('password');
});

it(': onBlur', async () => {
const onBlur = vi.fn();
const wrapper = mount(<Input label="标题" onBlur={onBlur} />);
await nextTick();
const input = wrapper.find('.t-input__wrap input');
await input.trigger('blur');
expect(onBlur).toBeCalled();
it(': autofocus', async () => {
const value = ref('123');
const wrapper = mount(<Input label="标题" v-model={value.value} clearable clearTrigger="focus" autofocus/>);
expect(wrapper.find('.t-icon-close-circle-filled').exists()).toBeTruthy();
wrapper.vm.blur();
await wrapper.vm.$nextTick();
expect(wrapper.find('.t-icon-close-circle-filled').exists()).toBeFalsy();
wrapper.vm.focus();
await wrapper.vm.$nextTick();
expect(wrapper.find('.t-icon-close-circle-filled').exists()).toBeTruthy();

});

it(': onFocus', async () => {
const onFocus = vi.fn();
const wrapper = mount(<Input label="标题" onFocus={onFocus} />);
const input = wrapper.find('.t-input__wrap input');
await input.trigger('focus');
expect(onFocus).toBeCalled();
it(': clearTrigger=always', async () => {
const value = ref('123');
const handleClear = vi.fn();
const wrapper = mount(<Input label="标题" v-model={value.value} clearable clearTrigger="always" onClear={handleClear} />);
expect(wrapper.find('.t-icon-close-circle-filled').exists()).toBeTruthy();
await wrapper.find('.t-icon-close-circle-filled').trigger('mousedown');
expect(value.value).toBe('');
expect(handleClear).toBeCalled();
});

it(': onChange', async () => {
const value = ref('');
const onChange = vi.fn();
const wrapper = mount(<Input label="标题" v-model={value.value} onChange={onChange} />);
const el = wrapper.find('.t-input__wrap input').element;
await simulateEvent(el, '文本', 'input');
expect(onChange).toBeCalledTimes(1);
expect(onChange).toHaveBeenCalledWith('文本');
it(': clearTrigger=focus', async () => {
const value = ref('123');
const handleClear = vi.fn();
const wrapper = mount(<Input label="标题" v-model={value.value} clearable clearTrigger="focus" onClear={handleClear} />);
expect(wrapper.find('.t-icon-close-circle-filled').exists()).toBeFalsy();
wrapper.vm.focus();
await wrapper.vm.$nextTick();
expect(wrapper.find('.t-icon-close-circle-filled').exists()).toBeTruthy();

await wrapper.find('.t-icon-close-circle-filled').trigger('mousedown');
expect(value.value).toBe('');
expect(handleClear).toBeCalled();
});
});
describe('event', async () => {
Expand Down Expand Up @@ -200,6 +210,33 @@ describe('Input.vue', async () => {
await $input.trigger('compositionend');
expect(onCompositionend).toBeCalled();
});

it(': onBlur', async () => {
const onBlur = vi.fn();
const wrapper = mount(<Input label="标题" onBlur={onBlur} />);
await nextTick();
const input = wrapper.find('.t-input__wrap input');
await input.trigger('blur');
expect(onBlur).toBeCalled();
});

it(': onFocus', async () => {
const onFocus = vi.fn();
const wrapper = mount(<Input label="标题" onFocus={onFocus} />);
const input = wrapper.find('.t-input__wrap input');
await input.trigger('focus');
expect(onFocus).toBeCalled();
});

it(': onChange', async () => {
const value = ref('');
const onChange = vi.fn();
const wrapper = mount(<Input label="标题" v-model={value.value} onChange={onChange} />);
const el = wrapper.find('.t-input__wrap input').element;
await simulateEvent(el, '文本', 'input');
expect(onChange).toBeCalledTimes(1);
expect(onChange).toHaveBeenCalledWith('文本');
});
});

describe('slots', async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/input/input.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ allowInputOverMax | Boolean | false | allow to continue input on value length is
autocomplete | String | undefined | attribute of input element, [see here](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) | N
autofocus | Boolean | false | autofocus on first rendered | N
borderless | Boolean | false | input without border | N
clearTrigger | String | always | show clear icon, clicked to clear input value。options: always / focus | N
clearable | Boolean | false | show clear icon, clicked to clear input value | N
disabled | Boolean | undefined | make input to be disabled | N
format | Function | - | input value formatter, `type=number` does not work. if you need to format number, `InputNumber` Component might be better。Typescript:`InputFormatType` `type InputFormatType = (value: InputValue) => string`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/input/type.ts) | N
Expand All @@ -22,7 +23,7 @@ maxlength | String / Number | - | \- | N
name | String | - | \- | N
placeholder | String | undefined | \- | N
prefixIcon | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
readonly | Boolean | false | \- | N
readonly | Boolean | undefined | \- | N
size | String | small | `deprecated`。options: small/medium。Typescript:`'medium' \| 'small'` | N
spellCheck | Boolean | false | attribute of input element, [see here](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/spellcheck) | N
status | String | undefined | options: default/success/warning/error | N
Expand Down
3 changes: 2 additions & 1 deletion src/input/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ allowInputOverMax | Boolean | false | 超出 `maxlength` 或 `maxcharacter` 之
autocomplete | String | undefined | 是否开启自动填充功能,HTML5 原生属性,[点击查看详情](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) | N
autofocus | Boolean | false | 自动聚焦 | N
borderless | Boolean | false | 是否开启无边框模式 | N
clearTrigger | String | always | 清空图标触发方式,仅在输入框有值时有效。可选项:always / focus | N
clearable | Boolean | false | 是否可清空 | N
disabled | Boolean | undefined | 是否禁用输入框 | N
format | Function | - | 【开发中】指定输入框展示值的格式。TS 类型:`InputFormatType` `type InputFormatType = (value: InputValue) => string`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/input/type.ts) | N
Expand All @@ -21,7 +22,7 @@ maxlength | String / Number | - | 用户最多可以输入的文本长度,一
name | String | - | 名称 | N
placeholder | String | undefined | 占位符 | N
prefixIcon | Slot / Function | - | 组件前置图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
readonly | Boolean | false | 只读状态 | N
readonly | Boolean | undefined | 只读状态 | N
size | String | small | 已废弃。输入框尺寸。可选项:small/medium。TS 类型:`'medium' \| 'small'` | N
spellCheck | Boolean | false | 是否开启拼写检查,HTML5 原生属性,[点击查看详情](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/spellcheck) | N
status | String | undefined | 输入框状态。默认情况会由组件内部根据实际情况呈现,如果文本过长引起的状态变化。可选项:default/success/warning/error | N
Expand Down
79 changes: 48 additions & 31 deletions src/input/input.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { PropType, ref, computed, defineComponent, toRefs, nextTick, watch } from 'vue';
import { PropType, ref, computed, defineComponent, nextTick, watch } from 'vue';
import {
BrowseIcon as TBrowseIcon,
BrowseOffIcon as TBrowseOffIcon,
CloseCircleFilledIcon as TCloseCircleFilledIcon,
} from 'tdesign-icons-vue-next';
import { useFocus } from '@vueuse/core';
import config from '../config';
import InputProps from './props';
import { InputValue, TdInputProps } from './type';
Expand Down Expand Up @@ -35,17 +34,16 @@ export default defineComponent({
},
},
setup(props, context) {
const readerTNodeJSX = useTNodeJSX();
const renderTNodeJSX = useTNodeJSX();
const inputClass = usePrefixClass('input');
const isDisabled = useFormDisabled();

const inputRef = ref();
const { autofocus } = toRefs(props);
const [innerValue] = useDefault<string, TdInputProps>(props, context.emit, 'value', 'change');

const status = props.status || 'default';
const renderType = ref(props.type);
const { focused } = useFocus(inputRef, { initialValue: props.autofocus });
const focused = ref(props.autofocus);

const inputClasses = computed(() => [
`${inputClass.value}__control`,
Expand All @@ -64,6 +62,14 @@ export default defineComponent({
[`${inputClass.value}--border`]: !props.borderless,
},
]);
const showClear = computed(() => {
if (isDisabled.value || props.readonly === true) return false;

if (props.clearable && innerValue.value && innerValue.value.length > 0) {
return props.clearTrigger === 'always' || (props.clearTrigger === 'focus' && focused.value);
}
return false;
});

const setInputValue = (v: InputValue = '') => {
const input = inputRef.value as HTMLInputElement;
Expand Down Expand Up @@ -102,24 +108,30 @@ export default defineComponent({

const focus = () => {
focused.value = true;
inputRef.value?.focus();
};

const blur = () => {
focused.value = false;
// inputRef.value?.blur();
inputRef.value?.blur();
};

extendAPI({ focus, blur });

const handleClear = (e: MouseEvent) => {
e.preventDefault();
innerValue.value = '';
focused.value = true;
focus();
props.onClear?.({ e });
};

const handleFocus = (e: FocusEvent) => {
focused.value = true;
props.onFocus?.(innerValue.value, { e });
};

const handleBlur = (e: FocusEvent) => {
focused.value = false;
props.onBlur?.(innerValue.value, { e });
};

Expand All @@ -131,13 +143,17 @@ export default defineComponent({
renderType.value = renderType.value === 'password' ? 'text' : 'password';
};

watch(autofocus, (autofocus, prevAutofocus) => {
if (autofocus === true) {
nextTick(() => {
focused.value = true;
});
}
});
watch(
() => props.autofocus,
(v) => {
if (v === true) {
nextTick(() => {
inputRef.value?.focus();
});
}
},
{ immediate: true },
);

watch(
() => props.type,
Expand All @@ -148,9 +164,9 @@ export default defineComponent({
);

return () => {
const readerPrefix = () => {
const prefixIcon = readerTNodeJSX('prefixIcon');
const label = readerTNodeJSX('label');
const renderPrefix = () => {
const prefixIcon = renderTNodeJSX('prefixIcon');
const label = renderTNodeJSX('label');

return (
<div class={`${inputClass.value}__wrap--prefix`}>
Expand All @@ -159,26 +175,27 @@ export default defineComponent({
</div>
);
};
const readerClearable = () => {
if (props.clearable && innerValue.value && innerValue.value.length > 0) {
const renderClearable = () => {
if (showClear.value) {
return (
<div class={`${inputClass.value}__wrap--clearable-icon`} onClick={handleClear}>
<div class={`${inputClass.value}__wrap--clearable-icon`} onMousedown={handleClear}>
<TCloseCircleFilledIcon />
</div>
);
}

return null;
};
const readerSuffix = () => {
const suffix = readerTNodeJSX('suffix');
const renderSuffix = () => {
const suffix = renderTNodeJSX('suffix');
if (!suffix) {
return null;
}
return <div class={`${inputClass.value}__wrap--suffix`}>{suffix}</div>;
};

const readerSuffixIcon = () => {
let suffixIcon = readerTNodeJSX('suffixIcon');
const renderSuffixIcon = () => {
let suffixIcon = renderTNodeJSX('suffixIcon');
if (props.type === 'password') {
if (renderType.value === 'password') {
suffixIcon = <TBrowseOffIcon onClick={handlePwdIconClick} />;
Expand All @@ -193,8 +210,8 @@ export default defineComponent({
return <div class={`${inputClass.value}__wrap--suffix-icon`}>{suffixIcon}</div>;
};

const readerTips = () => {
const tips = readerTNodeJSX('tips');
const renderTips = () => {
const tips = renderTNodeJSX('tips');
if (!tips) {
return null;
}
Expand All @@ -203,7 +220,7 @@ export default defineComponent({

return (
<div class={rootClasses.value}>
{readerPrefix()}
{renderPrefix()}
<div class={`${inputClass.value}__wrap`}>
<div class={`${inputClass.value}__content ${inputClass.value}--${status || 'default'}`}>
<input
Expand All @@ -225,11 +242,11 @@ export default defineComponent({
onInput={handleInput}
onCompositionend={handleCompositionend}
/>
{readerClearable()}
{readerSuffix()}
{readerSuffixIcon()}
{renderClearable()}
{renderSuffix()}
{renderSuffixIcon()}
</div>
{readerTips()}
{renderTips()}
</div>
</div>
);
Expand Down
14 changes: 13 additions & 1 deletion src/input/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ export default {
autofocus: Boolean,
/** 是否开启无边框模式 */
borderless: Boolean,
/** 清空图标触发方式,仅在输入框有值时有效 */
clearTrigger: {
type: String as PropType<TdInputProps['clearTrigger']>,
default: 'always' as TdInputProps['clearTrigger'],
validator(val: TdInputProps['clearTrigger']): boolean {
if (!val) return true;
return ['always', 'focus'].includes(val);
},
},
/** 是否可清空 */
clearable: Boolean,
/** 是否禁用输入框 */
Expand Down Expand Up @@ -75,7 +84,10 @@ export default {
type: Function as PropType<TdInputProps['prefixIcon']>,
},
/** 只读状态 */
readonly: Boolean,
readonly: {
type: Boolean,
default: undefined,
},
/** 是否开启拼写检查,HTML5 原生属性,[点击查看详情](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/spellcheck) */
spellCheck: Boolean,
/** 输入框状态。默认情况会由组件内部根据实际情况呈现,如果文本过长引起的状态变化 */
Expand Down
5 changes: 5 additions & 0 deletions src/input/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export interface TdInputProps {
* @default false
*/
borderless?: boolean;
/**
* 清空图标触发方式,仅在输入框有值时有效
* @default always
*/
clearTrigger?: 'always' | 'focus';
/**
* 是否可清空
* @default false
Expand Down
Loading