From aae1f800aaed2c9bb18c0478df9e6fd331b1bf8c Mon Sep 17 00:00:00 2001 From: zcj <18137693952@163.com> Date: Mon, 12 Sep 2022 15:50:48 +0800 Subject: [PATCH 1/8] =?UTF-8?q?fix(tag-input):=20=E7=A7=BB=E9=99=A4=20rend?= =?UTF-8?q?er=20setup=E7=9B=B4=E6=8E=A5=E5=AF=BC=E5=87=BA=EF=BC=8C?= =?UTF-8?q?=E6=89=93=E5=BC=80=E5=85=B3=E9=97=ADsuggestionList=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E9=80=BB=E8=BE=91=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devui/tag-input/src/tag-input.tsx | 169 +++++++----------- 1 file changed, 68 insertions(+), 101 deletions(-) diff --git a/packages/devui-vue/devui/tag-input/src/tag-input.tsx b/packages/devui-vue/devui/tag-input/src/tag-input.tsx index 3f4fad5da7..7217bba29e 100644 --- a/packages/devui-vue/devui/tag-input/src/tag-input.tsx +++ b/packages/devui-vue/devui/tag-input/src/tag-input.tsx @@ -1,5 +1,6 @@ import { defineComponent, ref, computed, nextTick, watch, SetupContext, getCurrentInstance } from 'vue'; import { createI18nTranslate } from '../../locale/create'; +import clickoutsideDirective from '../../shared/devui-directive/clickoutside'; import removeBtnSvg from './icon-remove'; import { Suggestion, TagInputProps, tagInputProps } from './tag-input-types'; import './tag-input.scss'; @@ -14,6 +15,9 @@ const KEYS_MAP = { export default defineComponent({ name: 'DTagInput', + directives: { + clickoutside: clickoutsideDirective, + }, props: tagInputProps, emits: ['update:tags', 'update:suggestionList', 'valueChange'], setup(props: TagInputProps, ctx: SetupContext) { @@ -69,8 +73,14 @@ export default defineComponent({ isInputBoxFocus.value = true; }; const onInputBlur = () => { + // isInputBoxFocus.value = false; + }; + + // 点击元素外部区域关闭Suggestion选择 + const closeSuggestion = () => { isInputBoxFocus.value = false; }; + const handleEnter = () => { let res = { [props.displayProperty]: tagInputVal.value }; if (tagInputVal.value === '' && mergedSuggestions.value.length === 0) { @@ -119,20 +129,23 @@ export default defineComponent({ } }; - const removeTag = ($event: MouseEvent, tagIdx: number) => { + const removeTag = ($event: Event, tagIdx: number) => { $event.preventDefault(); ctx.emit('update:suggestionList', add(props.suggestionList, props.tags[tagIdx])); const newTags = remove(props.tags, tagIdx); ctx.emit('valueChange', props.tags, newTags); ctx.emit('update:tags', newTags); + nextTick(() => { tagInputRef.value?.focus(); }); }; - const onSuggestionItemClick = ($event: MouseEvent, itemIndex: number) => { + const onSuggestionItemClick = ($event: Event, itemIndex: number) => { $event.preventDefault(); const target = mergedSuggestions.value[itemIndex]; const newTags = add(props.tags, target); + + const newSuggestions = remove(props.suggestionList, target.__index); ctx.emit('valueChange', props.tags, newTags); ctx.emit('update:tags', newTags); @@ -144,119 +157,73 @@ export default defineComponent({ return !props.disabled && !isTagsLimit.value && isInputBoxFocus.value; }); - return { - tagInputRef, - tagInputVal, - isInputBoxFocus, - onInput, - onInputFocus, - onInputBlur, - removeTag, - onSuggestionItemClick, - onInputKeydown, - isShowSuggestion, - mergedSuggestions, - selectIndex, - isTagsLimit, - t, - }; - }, - render() { - const { - tagInputVal, - isInputBoxFocus, - disabled, - disabledText, - isTagsLimit, - maxTagsText, - displayProperty, - tags, - onInputKeydown, - onInputFocus, - onInputBlur, - onInput, - onSuggestionItemClick, - removeTag, - placeholder, - spellcheck, - isShowSuggestion, - noData, - mergedSuggestions, - selectIndex, - maxTags, - t, - } = this; - const inputBoxCls = { 'devui-tags': true, 'devui-form-control': true, 'devui-dropdown-origin': true, 'devui-dropdown-origin-open': isInputBoxFocus, - 'devui-disabled': disabled, + 'devui-disabled': props.disabled, }; const tagInputCls = { input: true, 'devui-input': true, 'invalid-tag': false, }; - const tagInputStyle = [`display:${disabled ? 'none' : 'block'};`]; + const tagInputStyle = [`display:${props.disabled ? 'none' : 'block'};`]; - const noDataTpl =
  • {noData}
  • ; + const noDataTpl =
  • {props.noData}
  • ; - return ( -
    -
    -
    ); }, }); From fe9896e2c2e5c3f6041db51ddea3b2e202f3dd62 Mon Sep 17 00:00:00 2001 From: zcj <18137693952@163.com> Date: Mon, 12 Sep 2022 17:10:22 +0800 Subject: [PATCH 2/8] =?UTF-8?q?feat(tag-input):=20suggestion-list=20?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20FlexibleOverlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/{ => components}/icon-remove.tsx | 0 .../devui/tag-input/src/tag-input.scss | 10 +-- .../devui/tag-input/src/tag-input.tsx | 69 +++++++++++-------- 3 files changed, 40 insertions(+), 39 deletions(-) rename packages/devui-vue/devui/tag-input/src/{ => components}/icon-remove.tsx (100%) diff --git a/packages/devui-vue/devui/tag-input/src/icon-remove.tsx b/packages/devui-vue/devui/tag-input/src/components/icon-remove.tsx similarity index 100% rename from packages/devui-vue/devui/tag-input/src/icon-remove.tsx rename to packages/devui-vue/devui/tag-input/src/components/icon-remove.tsx diff --git a/packages/devui-vue/devui/tag-input/src/tag-input.scss b/packages/devui-vue/devui/tag-input/src/tag-input.scss index 76b7630a87..9ffe7295ab 100644 --- a/packages/devui-vue/devui/tag-input/src/tag-input.scss +++ b/packages/devui-vue/devui/tag-input/src/tag-input.scss @@ -153,18 +153,10 @@ } .#{$devui-prefix}-tags-autocomplete { - position: absolute; - padding-bottom: 5px; - z-index: $devui-z-index-dropdown; width: 100%; background-color: $devui-connected-overlay-bg; box-shadow: $devui-shadow-length-connected-overlay $devui-shadow; - &.#{$devui-prefix}-dropdown-menu { - display: block; - margin: 4px 0; - } - .#{$devui-prefix}-suggestion-list { margin: 0; padding: 0; @@ -191,7 +183,7 @@ &.selected { color: $devui-brand; - background-color: $devui-list-item-hover-bg; + background-color: $devui-list-item-active-bg; } } } diff --git a/packages/devui-vue/devui/tag-input/src/tag-input.tsx b/packages/devui-vue/devui/tag-input/src/tag-input.tsx index 7217bba29e..214a1681c6 100644 --- a/packages/devui-vue/devui/tag-input/src/tag-input.tsx +++ b/packages/devui-vue/devui/tag-input/src/tag-input.tsx @@ -1,7 +1,8 @@ -import { defineComponent, ref, computed, nextTick, watch, SetupContext, getCurrentInstance } from 'vue'; +import { defineComponent, ref, computed, nextTick, watch, SetupContext, getCurrentInstance, Teleport, Transition } from 'vue'; import { createI18nTranslate } from '../../locale/create'; -import clickoutsideDirective from '../../shared/devui-directive/clickoutside'; -import removeBtnSvg from './icon-remove'; +import ClickOutside from '../../shared/devui-directive/clickoutside'; +import { FlexibleOverlay } from '../../overlay/src/flexible-overlay'; +import removeBtnSvg from './components/icon-remove'; import { Suggestion, TagInputProps, tagInputProps } from './tag-input-types'; import './tag-input.scss'; @@ -16,7 +17,7 @@ const KEYS_MAP = { export default defineComponent({ name: 'DTagInput', directives: { - clickoutside: clickoutsideDirective, + ClickOutside, }, props: tagInputProps, emits: ['update:tags', 'update:suggestionList', 'valueChange'], @@ -67,14 +68,11 @@ export default defineComponent({ selectIndex.value > 0 ? selectIndex.value-- : (selectIndex.value = mergedSuggestions.value.length - 1); }; - const tagInputRef = ref(null); + const tagInputRef = ref(); const isInputBoxFocus = ref(false); const onInputFocus = () => { isInputBoxFocus.value = true; }; - const onInputBlur = () => { - // isInputBoxFocus.value = false; - }; // 点击元素外部区域关闭Suggestion选择 const closeSuggestion = () => { @@ -173,7 +171,9 @@ export default defineComponent({ const noDataTpl =
  • {props.noData}
  • ; - return () => (
    + const origin = ref(); + + return () => (
      {props.tags.map((tag, tagIdx) => { @@ -197,33 +197,42 @@ export default defineComponent({ style={tagInputStyle} onKeyDown={onInputKeydown} onFocus={onInputFocus} - onBlur={onInputBlur} onInput={($event: InputEvent) => onInput($event)} placeholder={isTagsLimit.value ? `${props.maxTagsText || t('maxTagsText')} ${props.maxTags}` : props.placeholder} spellCheck={props.spellcheck} disabled={isTagsLimit.value} />
    - {isShowSuggestion.value && ( -
    -
      - {mergedSuggestions.value.length === 0 - ? noDataTpl - : mergedSuggestions.value.map((item: Suggestion, index: number) => { - return ( -
    • { - onSuggestionItemClick($event, index); - }} - > - {item[props.displayProperty]} -
    • - ); - })} -
    -
    - )} + + + + +
    +
      + {mergedSuggestions.value.length === 0 + ? noDataTpl + : mergedSuggestions.value.map((item: Suggestion, index: number) => { + return ( +
    • { + $event.stopPropagation(); + onSuggestionItemClick($event, index); + }} + > + {item[props.displayProperty]} +
    • + ); + })} +
    +
    +
    +
    +
    ); }, }); From 6d63ba2e9ec58accd982f63587ade1afc8b1b070 Mon Sep 17 00:00:00 2001 From: zcj <18137693952@163.com> Date: Mon, 12 Sep 2022 17:13:38 +0800 Subject: [PATCH 3/8] fix(tag-input): valueChange to change --- packages/devui-vue/devui/tag-input/src/tag-input.tsx | 8 ++++---- packages/devui-vue/docs/components/tag-input/index.md | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/devui-vue/devui/tag-input/src/tag-input.tsx b/packages/devui-vue/devui/tag-input/src/tag-input.tsx index 214a1681c6..a2cd8be830 100644 --- a/packages/devui-vue/devui/tag-input/src/tag-input.tsx +++ b/packages/devui-vue/devui/tag-input/src/tag-input.tsx @@ -20,7 +20,7 @@ export default defineComponent({ ClickOutside, }, props: tagInputProps, - emits: ['update:tags', 'update:suggestionList', 'valueChange'], + emits: ['update:tags', 'update:suggestionList', 'change'], setup(props: TagInputProps, ctx: SetupContext) { const app = getCurrentInstance(); const t = createI18nTranslate('DTagInput', app); @@ -102,7 +102,7 @@ export default defineComponent({ } const newTags = add(props.tags, res); - ctx.emit('valueChange', props.tags, newTags); + ctx.emit('change', props.tags, newTags); ctx.emit('update:tags', newTags); mergedSuggestions.value.length === 0 && (tagInputVal.value = ''); }; @@ -131,7 +131,7 @@ export default defineComponent({ $event.preventDefault(); ctx.emit('update:suggestionList', add(props.suggestionList, props.tags[tagIdx])); const newTags = remove(props.tags, tagIdx); - ctx.emit('valueChange', props.tags, newTags); + ctx.emit('change', props.tags, newTags); ctx.emit('update:tags', newTags); nextTick(() => { @@ -145,7 +145,7 @@ export default defineComponent({ const newSuggestions = remove(props.suggestionList, target.__index); - ctx.emit('valueChange', props.tags, newTags); + ctx.emit('change', props.tags, newTags); ctx.emit('update:tags', newTags); ctx.emit('update:suggestionList', newSuggestions); }; diff --git a/packages/devui-vue/docs/components/tag-input/index.md b/packages/devui-vue/docs/components/tag-input/index.md index 5462d79f47..6a24099df1 100644 --- a/packages/devui-vue/docs/components/tag-input/index.md +++ b/packages/devui-vue/docs/components/tag-input/index.md @@ -20,7 +20,7 @@ no-data="暂无数据" :minLength='1' :caseSensitivity="true" - @valueChange="changeValue" + @change="changeValue" @update:tags="changeTags" @update:suggestionList="changeSuggestionList" > @@ -32,7 +32,7 @@ export default defineComponent({ setup() { const state = reactive({ tags: [{ name: "123" }], - suggestionList: [{ name: "item1" }] + suggestionList: [{ name: "item1" },{ name: "item2" },{ name: "item3" }, { name: "item4" }] }) const changeTags = (val) => { console.log(val) @@ -140,6 +140,6 @@ export default defineComponent({ | 事件 | 说明 | 跳转 Demo | | :---------: | :------------------------------------------------------- | --------------------- | -| valueChange | 当选中某个选项项后,将会调用此函数,参数为当前选择项的值 | [基本用法](#基本用法) | +| change | 当选中某个选项项后,将会调用此函数,参数为当前选择项的值 | [基本用法](#基本用法) | | update:tags | 当选项数据发生改变时,返回新的标签列表 | [基本用法](#基本用法) | | update:suggestionList | 当选项数据发生变化时,返回新的可选择标签列表 | [基本用法](#基本用法) | From e5634e6cde44326439139faf7c408ac7e705eeff Mon Sep 17 00:00:00 2001 From: zcj <18137693952@163.com> Date: Mon, 12 Sep 2022 21:52:09 +0800 Subject: [PATCH 4/8] =?UTF-8?q?feat(tag-input):=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=B0=86=E9=83=A8=E5=88=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E6=8A=BD=E7=A6=BB=E6=88=90=E5=87=BD=E6=95=B0=E3=80=81=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E8=8E=B7=E5=8F=96=E7=88=B6=E5=85=83=E7=B4=A0=E5=AE=BD?= =?UTF-8?q?=E5=BA=A6=E7=9A=84=E9=80=BB=E8=BE=91=E7=94=A8=E4=BA=8E=E8=AE=BE?= =?UTF-8?q?=E7=BD=AEsuggestion-list=E7=9A=84=E5=AE=BD=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devui/tag-input/src/tag-input.scss | 5 +- .../devui/tag-input/src/tag-input.tsx | 172 +++++++++++------- 2 files changed, 114 insertions(+), 63 deletions(-) diff --git a/packages/devui-vue/devui/tag-input/src/tag-input.scss b/packages/devui-vue/devui/tag-input/src/tag-input.scss index 9ffe7295ab..b79c7edc30 100644 --- a/packages/devui-vue/devui/tag-input/src/tag-input.scss +++ b/packages/devui-vue/devui/tag-input/src/tag-input.scss @@ -5,7 +5,7 @@ outline: none; } -.#{$devui-prefix}-tags-host { +.#{$devui-prefix}-tag-input { position: relative; height: 100%; outline: none; @@ -154,6 +154,8 @@ .#{$devui-prefix}-tags-autocomplete { width: 100%; + padding: 8px; + border-radius: $devui-border-radius; background-color: $devui-connected-overlay-bg; box-shadow: $devui-shadow-length-connected-overlay $devui-shadow; @@ -172,6 +174,7 @@ text-overflow: ellipsis; font-size: $devui-font-size; line-height: 20px; + border-radius: $devui-border-radius; &:not(.#{$devui-prefix}-disabled) { cursor: pointer; diff --git a/packages/devui-vue/devui/tag-input/src/tag-input.tsx b/packages/devui-vue/devui/tag-input/src/tag-input.tsx index a2cd8be830..5899b91361 100644 --- a/packages/devui-vue/devui/tag-input/src/tag-input.tsx +++ b/packages/devui-vue/devui/tag-input/src/tag-input.tsx @@ -1,7 +1,20 @@ -import { defineComponent, ref, computed, nextTick, watch, SetupContext, getCurrentInstance, Teleport, Transition } from 'vue'; +import { + defineComponent, + ref, + computed, + nextTick, + watch, + SetupContext, + getCurrentInstance, + Teleport, + Transition, + onMounted, + onUnmounted, +} from 'vue'; import { createI18nTranslate } from '../../locale/create'; import ClickOutside from '../../shared/devui-directive/clickoutside'; import { FlexibleOverlay } from '../../overlay/src/flexible-overlay'; +import { useNamespace } from '../../shared/hooks/use-namespace'; import removeBtnSvg from './components/icon-remove'; import { Suggestion, TagInputProps, tagInputProps } from './tag-input-types'; import './tag-input.scss'; @@ -25,11 +38,14 @@ export default defineComponent({ const app = getCurrentInstance(); const t = createI18nTranslate('DTagInput', app); + const ns = useNamespace('tag-input'); + const add = (arr: Suggestion[], target: Suggestion) => { const res = Object.assign({}, target); delete res.__index; return arr.concat(res); }; + const remove = (arr: Suggestion[], targetIdx: number) => { const newArr = arr.slice(); newArr.splice(targetIdx, 1); @@ -41,6 +57,7 @@ export default defineComponent({ const v = ($event.target as HTMLInputElement).value || ''; tagInputVal.value = v.trim(); }; + const mergedSuggestions = computed(() => { let suggestions = props.suggestionList.map((item, index: number) => { return { @@ -51,6 +68,7 @@ export default defineComponent({ if (tagInputVal.value === '') { return suggestions; } + return (suggestions = props.caseSensitivity ? suggestions.filter((item) => item[props.displayProperty].indexOf(tagInputVal.value) !== -1) : suggestions.filter((item) => item[props.displayProperty].toLowerCase().indexOf(tagInputVal.value.toLowerCase()) !== -1)); @@ -60,6 +78,7 @@ export default defineComponent({ watch(mergedSuggestions, () => { selectIndex.value = 0; }); + const onSelectIndexChange = (isUp = false) => { if (isUp) { selectIndex.value < mergedSuggestions.value.length - 1 ? selectIndex.value++ : (selectIndex.value = 0); @@ -106,6 +125,7 @@ export default defineComponent({ ctx.emit('update:tags', newTags); mergedSuggestions.value.length === 0 && (tagInputVal.value = ''); }; + const onInputKeydown = ($event: KeyboardEvent) => { switch ($event.key) { case KEYS_MAP.tab: @@ -136,14 +156,14 @@ export default defineComponent({ nextTick(() => { tagInputRef.value?.focus(); + isInputBoxFocus.value = true; }); }; + const onSuggestionItemClick = ($event: Event, itemIndex: number) => { $event.preventDefault(); const target = mergedSuggestions.value[itemIndex]; const newTags = add(props.tags, target); - - const newSuggestions = remove(props.suggestionList, target.__index); ctx.emit('change', props.tags, newTags); ctx.emit('update:tags', newTags); @@ -155,47 +175,104 @@ export default defineComponent({ return !props.disabled && !isTagsLimit.value && isInputBoxFocus.value; }); - const inputBoxCls = { - 'devui-tags': true, - 'devui-form-control': true, - 'devui-dropdown-origin': true, - 'devui-dropdown-origin-open': isInputBoxFocus, - 'devui-disabled': props.disabled, + + // 已选择 tags 列表 + const chosenTags = () => { + return ; }; + + const noDataTpl =
  • {props.noData}
  • ; + + const origin = ref(); + const dropdownWidth = ref('0'); + const updateDropdownWidth = () => { + dropdownWidth.value = origin?.value?.clientWidth ? origin.value.clientWidth + 'px' : '100%'; + }; + + onMounted(() => { + updateDropdownWidth(); + window.addEventListener('resize', updateDropdownWidth); + }); + + onUnmounted(() => { + window.removeEventListener('resize', updateDropdownWidth); + }); + + // 选择建议列表 + const suggestionList = () => { + const showNoData = mergedSuggestions.value.length === 0; + const suggestionListItem = mergedSuggestions.value.map((item: Suggestion, index: number) => { + return ( +
  • { + onSuggestionItemClick($event, index); + }} + > + {item[props.displayProperty]} +
  • + ); + }); + + return + + +
    +
      + {showNoData ? noDataTpl : suggestionListItem} +
    +
    +
    +
    +
    ; + }; + + const inputBoxCls = computed(() => { + return { + 'devui-tags': true, + 'devui-form-control': true, + 'devui-dropdown-origin': true, + 'devui-dropdown-origin-open': isInputBoxFocus.value, + 'devui-disabled': props.disabled, + }; + }); + const tagInputCls = { input: true, 'devui-input': true, 'invalid-tag': false, }; - const tagInputStyle = [`display:${props.disabled ? 'none' : 'block'};`]; - - const noDataTpl =
  • {props.noData}
  • ; - - const origin = ref(); + const tagInputStyle = computed(() => { + return [`display:${props.disabled ? 'none' : 'block'};`]; + }); - return () => (
    -
    - + return () => (
    +
    + {chosenTags()} onInput($event)} placeholder={isTagsLimit.value ? `${props.maxTagsText || t('maxTagsText')} ${props.maxTags}` : props.placeholder} @@ -203,36 +280,7 @@ export default defineComponent({ disabled={isTagsLimit.value} />
    - - - - -
    -
      - {mergedSuggestions.value.length === 0 - ? noDataTpl - : mergedSuggestions.value.map((item: Suggestion, index: number) => { - return ( -
    • { - $event.stopPropagation(); - onSuggestionItemClick($event, index); - }} - > - {item[props.displayProperty]} -
    • - ); - })} -
    -
    -
    -
    -
    + {suggestionList()}
    ); }, }); From 4e0e786f749db95f469871c080375296b3d74640 Mon Sep 17 00:00:00 2001 From: zcj <18137693952@163.com> Date: Mon, 12 Sep 2022 21:53:28 +0800 Subject: [PATCH 5/8] =?UTF-8?q?test(tag-input):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=9B=A0=E4=B8=BA=E9=80=BB=E8=BE=91=E6=9B=B4=E6=94=B9=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E7=9A=84=E6=B5=8B=E8=AF=95=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tag-input/__tests__/tag-input.spec.ts | 101 ++++++++++++------ 1 file changed, 67 insertions(+), 34 deletions(-) diff --git a/packages/devui-vue/devui/tag-input/__tests__/tag-input.spec.ts b/packages/devui-vue/devui/tag-input/__tests__/tag-input.spec.ts index 8779faef3d..88ef01256b 100644 --- a/packages/devui-vue/devui/tag-input/__tests__/tag-input.spec.ts +++ b/packages/devui-vue/devui/tag-input/__tests__/tag-input.spec.ts @@ -1,24 +1,28 @@ import { mount } from '@vue/test-utils'; import { reactive, nextTick } from 'vue'; import DTagInput from '../src/tag-input'; +import { useNamespace } from '../../shared/hooks/use-namespace'; jest.mock('../../locale/create', () => ({ createI18nTranslate: () => jest.fn(), })); +const ns = useNamespace('tag-input', true); + const customMount = (state) => mount({ components: { DTagInput }, template: ` + displayProperty="cname" + > `, - setup () { + setup() { return { - state + state, }; - } + }, }); describe('DTagInput', () => { @@ -27,17 +31,16 @@ describe('DTagInput', () => { tags: [ { cname: 'Y.Chen' }, { cname: 'b' }, - { cname: 'c' } + { cname: 'c' }, ], suggestionList: [ { cname: 'd' }, { cname: 'e' }, { cname: 'f' }, - ] + ], }); const wrapper = customMount(state); - - expect(wrapper.find('.devui-tags-host').exists()).toBe(true); + expect(wrapper.find(ns.b()).exists()).toBe(true); expect(wrapper.find('.devui-tags').exists()).toBe(true); expect(wrapper.find('.devui-tag-list').exists()).toBe(true); expect(wrapper.find('.devui-input').exists()).toBe(true); @@ -49,6 +52,8 @@ describe('DTagInput', () => { state.tags[0] = { cname: 'X.Zhang' }; await nextTick(); expect(itemA.text()).toBe('X.Zhang'); + + wrapper.unmount(); }); it('tag-input show suggestion work', async () => { @@ -58,14 +63,19 @@ describe('DTagInput', () => { ], suggestionList: [ { cname: 'b' }, - ] + ], }); const wrapper = customMount(state); const input = wrapper.find('input.devui-input'); expect(wrapper.find('.devui-suggestion-list').exists()).toBe(false); await input.trigger('focus'); - expect(wrapper.find('.devui-suggestion-list').exists()).toBe(true); + + // 是否存在 devui-suggestion-list + const suggestionList = !!document.querySelectorAll('.devui-suggestion-list')[0]; + expect(suggestionList).toBe(true); + + wrapper.unmount(); }); it('tag-input disabled work', async () => { @@ -79,19 +89,21 @@ describe('DTagInput', () => { props: { tags, suggestionList, - disabled: false - } + disabled: false, + }, }); expect(wrapper.find('.devui-disabled').exists()).toBe(false); expect(wrapper.find('.devui-input').isVisible()).toBe(true); await wrapper.setProps({ - disabled: true + disabled: true, }); expect(wrapper.find('.devui-disabled').exists()).toBe(true); expect(wrapper.find('.devui-input').isVisible()).toBe(false); expect(wrapper.find('.remove-button').exists()).toBe(false); + + wrapper.unmount(); }); it('tag-input maxTags work', () => { @@ -106,11 +118,13 @@ describe('DTagInput', () => { props: { tags, suggestionList, - maxTags: 1 - } + maxTags: 1, + }, }); expect(wrapper.find('input').attributes('disabled')).toBe(''); + + wrapper.unmount(); }); it('tag-input removeTag work', async () => { @@ -121,14 +135,16 @@ describe('DTagInput', () => { ], suggestionList: [ { cname: 'c' }, - ] + ], }); const wrapper = customMount(state); const removeSvg = wrapper.find('.remove-button'); - await removeSvg.trigger('mousedown'); + await removeSvg.trigger('click'); expect(wrapper.findAll('.devui-tag-item').length).toBe(1); expect(state.tags.length).toBe(1); expect(state.suggestionList.length).toBe(2); + + wrapper.unmount(); }); it('tag-input keydown work', async () => { @@ -139,8 +155,8 @@ describe('DTagInput', () => { ], suggestionList: [ { cname: 'c' }, - { cname: 'xyz' } - ] + { cname: 'xyz' }, + ], }); const wrapper = customMount(state); const input = wrapper.find('input'); @@ -154,6 +170,8 @@ describe('DTagInput', () => { expect(state.tags.length).toBe(4); expect(state.tags[3].cname).toBe('xyz'); expect(state.suggestionList.length).toBe(1); + + wrapper.unmount(); }); it('tag-input filter suggestion work', async () => { @@ -165,22 +183,27 @@ describe('DTagInput', () => { suggestionList: [ { cname: 'x' }, { cname: 'xy' }, - { cname: 'xyz' } - ] + { cname: 'xyz' }, + ], }); const wrapper = customMount(state); const input = wrapper.find('input'); await input.trigger('focus'); - expect(wrapper.findAll('.devui-suggestion-item').length).toBe(3); + let suggestionList = document.querySelectorAll('.devui-suggestion-item'); + expect(suggestionList.length).toBe(3); await input.setValue('xy'); await input.trigger('input'); - expect(wrapper.findAll('.devui-suggestion-item').length).toBe(2); + suggestionList = document.querySelectorAll('.devui-suggestion-item'); + expect(suggestionList.length).toBe(2); await input.setValue('xxx'); await input.trigger('input'); - expect(wrapper.findAll('.devui-suggestion-item.devui-disabled').length).toBe(1); + suggestionList = document.querySelectorAll('.devui-suggestion-item'); + expect(suggestionList.length).toBe(1); + + wrapper.unmount(); }); it('tag-input click suggestion work', async () => { @@ -192,17 +215,20 @@ describe('DTagInput', () => { suggestionList: [ { cname: 'x' }, { cname: 'yyy' }, - { cname: 'xyz' } - ] + { cname: 'xyz' }, + ], }); const wrapper = customMount(state); await wrapper.find('input').trigger('focus'); - const yyy = wrapper.findAll('.devui-suggestion-item')[1]; + const suggestionList = document.querySelectorAll('.devui-suggestion-item'); + const yyy = suggestionList[1]; + yyy.dispatchEvent(new Event('click')); - await yyy.trigger('mousedown'); expect(state.tags.length).toBe(3); expect(state.tags[2].cname).toBe('yyy'); expect(state.suggestionList.length).toBe(2); + + wrapper.unmount(); }); it('tag-input arrow work', async () => { @@ -214,24 +240,31 @@ describe('DTagInput', () => { suggestionList: [ { cname: 'x' }, { cname: 'yyy' }, - { cname: 'xyz' } - ] + { cname: 'xyz' }, + ], }); const wrapper = customMount(state); const input = wrapper.find('input'); await input.trigger('focus'); + let suggestionList = document.querySelectorAll('.devui-suggestion-item'); + // 获取焦点默认第一个选中 + expect(suggestionList[0].className).toContain('selected'); - expect(wrapper.findAll('.devui-suggestion-item')[0].classes()).toContain('selected'); - + // 按下 下箭头,选中第二个数组第一个 await input.trigger('keydown', { key: 'ArrowDown' }); - expect(wrapper.findAll('.devui-suggestion-item')[1].classes()).toContain('selected'); + suggestionList = document.querySelectorAll('.devui-suggestion-item'); + expect(suggestionList[1].className).toContain('selected'); await input.trigger('keydown', { key: 'ArrowUp' }); await input.trigger('keydown', { key: 'ArrowUp' }); - expect(wrapper.findAll('.devui-suggestion-item')[2].classes()).toContain('selected'); + suggestionList = document.querySelectorAll('.devui-suggestion-item'); + expect(suggestionList[2].className).toContain('selected'); + // 按下Enter选中数据 await input.trigger('keydown', { key: 'Enter' }); expect(state.tags[2].cname).toBe('xyz'); expect(state.suggestionList.length).toBe(2); + + wrapper.unmount(); }); }); From 42c9b065ac66a14a55a8933644224b127352f710 Mon Sep 17 00:00:00 2001 From: zcj <18137693952@163.com> Date: Tue, 13 Sep 2022 10:16:20 +0800 Subject: [PATCH 6/8] feat(tag-input): add useInputKeydown --- .../src/composables/use-input-keydown.ts | 38 +++++++++++++++++++ .../devui/tag-input/src/tag-input-types.ts | 8 ++++ .../devui/tag-input/src/tag-input.tsx | 30 +-------------- 3 files changed, 48 insertions(+), 28 deletions(-) create mode 100644 packages/devui-vue/devui/tag-input/src/composables/use-input-keydown.ts diff --git a/packages/devui-vue/devui/tag-input/src/composables/use-input-keydown.ts b/packages/devui-vue/devui/tag-input/src/composables/use-input-keydown.ts new file mode 100644 index 0000000000..02da1ed97d --- /dev/null +++ b/packages/devui-vue/devui/tag-input/src/composables/use-input-keydown.ts @@ -0,0 +1,38 @@ +import { TagInputProps, HandleEnter, OnSelectIndexChange, UseInputKeydownReturnTypes } from '../tag-input-types'; + +export const useInputKeydown = ( + props: TagInputProps, + handleEnter: HandleEnter, + onSelectIndexChange: OnSelectIndexChange): UseInputKeydownReturnTypes => { + + const KEYS_MAP = { + tab: 'Tab', + down: 'ArrowDown', + up: 'ArrowUp', + enter: 'Enter', + space: ' ', + } as const; + + const onInputKeydown = ($event: KeyboardEvent) => { + switch ($event.key) { + case KEYS_MAP.tab: + case KEYS_MAP.enter: + case KEYS_MAP.space: + if (!props.isAddBySpace && KEYS_MAP.space) { + return; + } + handleEnter(); + break; + case KEYS_MAP.down: + onSelectIndexChange(true); + break; + case KEYS_MAP.up: + onSelectIndexChange(false); + break; + default: + break; + } + }; + + return { onInputKeydown }; +}; diff --git a/packages/devui-vue/devui/tag-input/src/tag-input-types.ts b/packages/devui-vue/devui/tag-input/src/tag-input-types.ts index 1e18ceef34..b06ad9c5ca 100644 --- a/packages/devui-vue/devui/tag-input/src/tag-input-types.ts +++ b/packages/devui-vue/devui/tag-input/src/tag-input-types.ts @@ -2,6 +2,7 @@ import type { ExtractPropTypes, PropType } from 'vue'; export interface Suggestion { __index?: number; + [x: string]: unknown; } @@ -65,3 +66,10 @@ export const tagInputProps = { } as const; export type TagInputProps = ExtractPropTypes; + +export type HandleEnter = () => void; +export type OnSelectIndexChange = (isUp: boolean) => void; + +export interface UseInputKeydownReturnTypes { + onInputKeydown: (e: KeyboardEvent) => void; +} diff --git a/packages/devui-vue/devui/tag-input/src/tag-input.tsx b/packages/devui-vue/devui/tag-input/src/tag-input.tsx index 5899b91361..0923c89f0b 100644 --- a/packages/devui-vue/devui/tag-input/src/tag-input.tsx +++ b/packages/devui-vue/devui/tag-input/src/tag-input.tsx @@ -18,14 +18,7 @@ import { useNamespace } from '../../shared/hooks/use-namespace'; import removeBtnSvg from './components/icon-remove'; import { Suggestion, TagInputProps, tagInputProps } from './tag-input-types'; import './tag-input.scss'; - -const KEYS_MAP = { - tab: 'Tab', - down: 'ArrowDown', - up: 'ArrowUp', - enter: 'Enter', - space: ' ', -} as const; +import { useInputKeydown } from './composables/use-input-keydown'; export default defineComponent({ name: 'DTagInput', @@ -126,26 +119,7 @@ export default defineComponent({ mergedSuggestions.value.length === 0 && (tagInputVal.value = ''); }; - const onInputKeydown = ($event: KeyboardEvent) => { - switch ($event.key) { - case KEYS_MAP.tab: - case KEYS_MAP.enter: - case KEYS_MAP.space: - if (!props.isAddBySpace && KEYS_MAP.space) { - return; - } - handleEnter(); - break; - case KEYS_MAP.down: - onSelectIndexChange(true); - break; - case KEYS_MAP.up: - onSelectIndexChange(); - break; - default: - break; - } - }; + const { onInputKeydown } = useInputKeydown(props, handleEnter, onSelectIndexChange); const removeTag = ($event: Event, tagIdx: number) => { $event.preventDefault(); From ba1aa5be2b67182dd007c66f10c31f5a24508404 Mon Sep 17 00:00:00 2001 From: zcj <18137693952@163.com> Date: Tue, 13 Sep 2022 11:00:20 +0800 Subject: [PATCH 7/8] =?UTF-8?q?feat(tag-input):=20v-model:tags=20=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=20v-model=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E3=80=81=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tag-input/__tests__/tag-input.spec.ts | 12 ++++-- .../devui/tag-input/src/tag-input-types.ts | 2 +- .../devui/tag-input/src/tag-input.tsx | 33 ++++++++++------- .../docs/components/tag-input/index.md | 37 +++++++++++++------ 4 files changed, 54 insertions(+), 30 deletions(-) diff --git a/packages/devui-vue/devui/tag-input/__tests__/tag-input.spec.ts b/packages/devui-vue/devui/tag-input/__tests__/tag-input.spec.ts index 88ef01256b..6edb124071 100644 --- a/packages/devui-vue/devui/tag-input/__tests__/tag-input.spec.ts +++ b/packages/devui-vue/devui/tag-input/__tests__/tag-input.spec.ts @@ -2,6 +2,12 @@ import { mount } from '@vue/test-utils'; import { reactive, nextTick } from 'vue'; import DTagInput from '../src/tag-input'; import { useNamespace } from '../../shared/hooks/use-namespace'; +import { Suggestion } from '../src/tag-input-types'; + +interface StateType { + tags: Array; + suggestionList: Array; +} jest.mock('../../locale/create', () => ({ createI18nTranslate: () => jest.fn(), @@ -9,11 +15,11 @@ jest.mock('../../locale/create', () => ({ const ns = useNamespace('tag-input', true); -const customMount = (state) => mount({ +const customMount = (state: StateType) => mount({ components: { DTagInput }, template: ` @@ -116,7 +122,7 @@ describe('DTagInput', () => { ]); const wrapper = mount(DTagInput, { props: { - tags, + modelValue: tags, suggestionList, maxTags: 1, }, diff --git a/packages/devui-vue/devui/tag-input/src/tag-input-types.ts b/packages/devui-vue/devui/tag-input/src/tag-input-types.ts index b06ad9c5ca..41ee6c497b 100644 --- a/packages/devui-vue/devui/tag-input/src/tag-input-types.ts +++ b/packages/devui-vue/devui/tag-input/src/tag-input-types.ts @@ -7,7 +7,7 @@ export interface Suggestion { } export const tagInputProps = { - tags: { + modelValue: { type: Array as PropType, default: (): [] => [], }, diff --git a/packages/devui-vue/devui/tag-input/src/tag-input.tsx b/packages/devui-vue/devui/tag-input/src/tag-input.tsx index 0923c89f0b..c4512517a6 100644 --- a/packages/devui-vue/devui/tag-input/src/tag-input.tsx +++ b/packages/devui-vue/devui/tag-input/src/tag-input.tsx @@ -26,13 +26,18 @@ export default defineComponent({ ClickOutside, }, props: tagInputProps, - emits: ['update:tags', 'update:suggestionList', 'change'], + emits: ['update:modelValue', 'update:suggestionList', 'change'], setup(props: TagInputProps, ctx: SetupContext) { const app = getCurrentInstance(); const t = createI18nTranslate('DTagInput', app); const ns = useNamespace('tag-input'); + const selectedTags = ref>([]); + watch(() => props.modelValue, () => { + selectedTags.value = props.modelValue; + }, { immediate: true, deep: true }); + const add = (arr: Suggestion[], target: Suggestion) => { const res = Object.assign({}, target); delete res.__index; @@ -96,7 +101,7 @@ export default defineComponent({ if (tagInputVal.value === '' && mergedSuggestions.value.length === 0) { return false; } - if (props.tags.findIndex((item) => item[props.displayProperty] === tagInputVal.value) > -1) { + if (selectedTags.value.findIndex((item) => item[props.displayProperty] === tagInputVal.value) > -1) { tagInputVal.value = ''; return false; } @@ -113,9 +118,9 @@ export default defineComponent({ ctx.emit('update:suggestionList', remove(props.suggestionList, target.__index)); } - const newTags = add(props.tags, res); - ctx.emit('change', props.tags, newTags); - ctx.emit('update:tags', newTags); + const newTags = add(selectedTags.value, res); + ctx.emit('change', selectedTags.value, newTags); + ctx.emit('update:modelValue', newTags); mergedSuggestions.value.length === 0 && (tagInputVal.value = ''); }; @@ -123,10 +128,10 @@ export default defineComponent({ const removeTag = ($event: Event, tagIdx: number) => { $event.preventDefault(); - ctx.emit('update:suggestionList', add(props.suggestionList, props.tags[tagIdx])); - const newTags = remove(props.tags, tagIdx); - ctx.emit('change', props.tags, newTags); - ctx.emit('update:tags', newTags); + ctx.emit('update:suggestionList', add(props.suggestionList, selectedTags.value[tagIdx])); + const newTags = remove(selectedTags.value, tagIdx); + ctx.emit('change', selectedTags.value, newTags); + ctx.emit('update:modelValue', newTags); nextTick(() => { tagInputRef.value?.focus(); @@ -137,14 +142,14 @@ export default defineComponent({ const onSuggestionItemClick = ($event: Event, itemIndex: number) => { $event.preventDefault(); const target = mergedSuggestions.value[itemIndex]; - const newTags = add(props.tags, target); + const newTags = add(selectedTags.value, target); const newSuggestions = remove(props.suggestionList, target.__index); - ctx.emit('change', props.tags, newTags); - ctx.emit('update:tags', newTags); + ctx.emit('change', selectedTags.value, newTags); + ctx.emit('update:modelValue', newTags); ctx.emit('update:suggestionList', newSuggestions); }; - const isTagsLimit = computed(() => props.maxTags <= props.tags.length); + const isTagsLimit = computed(() => props.maxTags <= selectedTags.value.length); const isShowSuggestion = computed(() => { return !props.disabled && !isTagsLimit.value && isInputBoxFocus.value; }); @@ -153,7 +158,7 @@ export default defineComponent({ // 已选择 tags 列表 const chosenTags = () => { return
      - {props.tags.map((tag, tagIdx) => { + {selectedTags.value.map((tag, tagIdx) => { return (
    • {tag[props.displayProperty]} diff --git a/packages/devui-vue/docs/components/tag-input/index.md b/packages/devui-vue/docs/components/tag-input/index.md index 6a24099df1..3a95c76e32 100644 --- a/packages/devui-vue/docs/components/tag-input/index.md +++ b/packages/devui-vue/docs/components/tag-input/index.md @@ -11,14 +11,15 @@ :::demo ```vue +