@veltra/desktop 1.3.5 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/components/action/action-group.vue.d.ts +1 -1
  2. package/dist/components/action/action.vue.d.ts +1 -1
  3. package/dist/components/cascade/cascade.js +8 -2
  4. package/dist/components/cascade/cascade.js.map +1 -1
  5. package/dist/components/cascade/style2.css +4 -0
  6. package/dist/components/date-range-picker/date-range-picker.js +5 -2
  7. package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
  8. package/dist/components/date-range-picker/style2.css +4 -0
  9. package/dist/components/empty/empty.vue.d.ts +1 -1
  10. package/dist/components/form-item/form-item.js.map +1 -1
  11. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js +2 -2
  12. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
  13. package/dist/components/form-item/style2.css +2 -1
  14. package/dist/components/grid-input/style2.css +3 -0
  15. package/dist/components/multi-select/multi-select.js +67 -40
  16. package/dist/components/multi-select/multi-select.js.map +1 -1
  17. package/dist/components/multi-select/multi-select.vue.d.ts +4 -4
  18. package/dist/components/multi-select/style2.css +23 -10
  19. package/dist/components/multi-tree-select/multi-tree-select.js +52 -35
  20. package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
  21. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +1 -1
  22. package/dist/components/multi-tree-select/style2.css +26 -16
  23. package/dist/components/progress-nodes/progress-nodes.vue.d.ts +1 -1
  24. package/dist/components/select/select.js +159 -95
  25. package/dist/components/select/select.js.map +1 -1
  26. package/dist/components/select/style2.css +4 -17
  27. package/dist/components/select/use-options.js +67 -26
  28. package/dist/components/select/use-options.js.map +1 -1
  29. package/dist/components/steps/steps.vue.d.ts +2 -2
  30. package/dist/components/tabs/tabs-horizontal.vue.d.ts +2 -2
  31. package/dist/components/tabs/tabs-vertical.vue.d.ts +2 -2
  32. package/dist/components/tabs/tabs.vue.d.ts +2 -2
  33. package/dist/components/textarea/style2.css +1 -0
  34. package/dist/components/textarea/textarea.vue.d.ts +1 -1
  35. package/dist/components/tree/tree.vue.d.ts +1 -1
  36. package/dist/components/tree-select/style2.css +6 -15
  37. package/dist/components/tree-select/tree-select.js +95 -41
  38. package/dist/components/tree-select/tree-select.js.map +1 -1
  39. package/dist/components/tree-select/tree-select.vue.d.ts +1 -1
  40. package/package.json +6 -6
  41. package/src/components/cascade/cascade.vue +15 -2
  42. package/src/components/cascade/style.scss +5 -0
  43. package/src/components/date-range-picker/date-range-picker.vue +9 -1
  44. package/src/components/date-range-picker/style.scss +5 -0
  45. package/src/components/form-item/form-item.vue +2 -2
  46. package/src/components/form-item/style.scss +2 -1
  47. package/src/components/grid-input/style.scss +1 -0
  48. package/src/components/multi-select/multi-select.vue +50 -19
  49. package/src/components/multi-select/style.scss +27 -4
  50. package/src/components/multi-tree-select/multi-tree-select.vue +34 -14
  51. package/src/components/multi-tree-select/style.scss +28 -9
  52. package/src/components/select/__test__/select.test.ts +110 -0
  53. package/src/components/select/select.vue +130 -46
  54. package/src/components/select/style.scss +5 -14
  55. package/src/components/select/use-options.ts +83 -39
  56. package/src/components/textarea/style.scss +1 -0
  57. package/src/components/tree-select/style.scss +4 -8
  58. package/src/components/tree-select/tree-select.vue +90 -17
@@ -4,7 +4,7 @@
4
4
  v-bind="$attrs"
5
5
  trigger="click"
6
6
  ref="dropdownRef"
7
- :class="[cls.b, cls.m(size), bem.is('disabled', disabled)]"
7
+ :class="[cls.b, cls.m(size), bem.is('disabled', disabled), bem.is('focus', dropdownVisible)]"
8
8
  :content-class="[cls.e('panel'), cls.em('panel', size), contentClass]"
9
9
  :content-style="contentStyle"
10
10
  :min-width="minWidth"
@@ -16,11 +16,11 @@
16
16
  >
17
17
  <!-- 触发 -->
18
18
  <template #trigger>
19
- <span :class="cls.e('placeholder')" v-show="!model?.length">
19
+ <span :class="cls.e('placeholder')" v-if="!filterable && !model?.length">
20
20
  {{ placeholder }}
21
21
  </span>
22
22
 
23
- <div v-if="model?.length" :class="cls.e('tags')">
23
+ <div v-if="model?.length || filterable" :class="cls.e('tags')">
24
24
  <u-tag
25
25
  v-for="option of tags"
26
26
  :key="option[valueKey]"
@@ -31,6 +31,19 @@
31
31
  {{ option[labelKey] }}
32
32
  </u-tag>
33
33
  <u-tag v-if="restTag" size="small"> {{ restTag }}+ </u-tag>
34
+
35
+ <!-- 内嵌查询输入框:承载过滤与回车创建 -->
36
+ <input
37
+ v-if="filterable"
38
+ ref="inputRef"
39
+ :class="cls.e('input')"
40
+ v-model="queryString"
41
+ :placeholder="model?.length ? '' : placeholder"
42
+ :disabled="disabled"
43
+ @click.stop="handleInputClick"
44
+ @focus="handleInputFocus"
45
+ @keydown.enter.prevent="handleCreateByEnter"
46
+ />
34
47
  </div>
35
48
 
36
49
  <transition name="zoom-in" mode="out-in">
@@ -62,19 +75,6 @@
62
75
  <span> 已选 {{ model?.length }}/{{ max ?? options?.length ?? 0 }} </span>
63
76
  </div>
64
77
 
65
- <!-- 过滤器 -->
66
- <div v-if="filterable" :class="cls.e('content-filter')">
67
- <u-input
68
- placeholder="输入关键字进行过滤"
69
- v-model="queryString"
70
- @keydown.enter.prevent="handleCreateByEnter"
71
- >
72
- <template #suffix>
73
- <u-icon><Search /></u-icon>
74
- </template>
75
- </u-input>
76
- </div>
77
-
78
78
  <!-- 多选列表 -->
79
79
  <!--
80
80
  虚拟化启用时内容容器 height 由 useVirtualizer 命令式写入;
@@ -143,10 +143,18 @@
143
143
  <script lang="ts" setup>
144
144
  import { o as chainObj } from '@cat-kit/core'
145
145
  import { useFormFallbackProps, useVirtualizer } from '@veltra/compositions'
146
- import { ArrowDown, Close, Search } from '@veltra/icons/normal'
146
+ import { ArrowDown, Close } from '@veltra/icons/normal'
147
147
  import { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'
148
148
  import { injectFormContext } from '@veltra/utils'
149
- import { computed, shallowRef, shallowReactive, watch, provide } from 'vue'
149
+ import {
150
+ computed,
151
+ nextTick,
152
+ shallowRef,
153
+ shallowReactive,
154
+ watch,
155
+ provide,
156
+ useTemplateRef
157
+ } from 'vue'
150
158
 
151
159
  import type {
152
160
  MultiSelectEmits,
@@ -158,7 +166,6 @@ import { UCheckbox } from '../checkbox'
158
166
  import { UDropdown } from '../dropdown'
159
167
  import { UEmpty } from '../empty'
160
168
  import { UIcon } from '../icon'
161
- import { UInput } from '../input'
162
169
  import { UScroll } from '../scroll'
163
170
  import { useOptions } from '../select/use-options'
164
171
  import { UTag } from '../tag'
@@ -319,12 +326,35 @@ const restTag = computed(() => {
319
326
  return (model.value?.length ?? 0) - tags.value.length
320
327
  })
321
328
 
329
+ const dropdownVisible = shallowRef(false)
330
+
331
+ const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
332
+
322
333
  const handleDropdownVisible = (visible: boolean) => {
334
+ dropdownVisible.value = visible
323
335
  if (!visible) {
324
336
  queryString.value = ''
325
337
  }
326
338
  }
327
339
 
340
+ /**
341
+ * 拦截输入区域的点击(@click.stop):阻止 dropdown 的 trigger 开合切换,
342
+ * 保持面板展开以不中断输入。
343
+ */
344
+ const handleInputClick = () => {
345
+ if (!dropdownVisible.value) dropdownRef.value?.open()
346
+ }
347
+
348
+ const handleInputFocus = () => {
349
+ if (!dropdownVisible.value) dropdownRef.value?.open()
350
+ }
351
+
352
+ /** 勾选后把焦点还给输入框,便于连续输入过滤 */
353
+ const refocusInput = () => {
354
+ if (!filterable.value) return
355
+ nextTick(() => inputRef.value?.focus())
356
+ }
357
+
328
358
  const handleCheck = (option: Record<string, any>, checked: boolean) => {
329
359
  if (checked) {
330
360
  if (option.__isTemp && props.creatable) {
@@ -352,6 +382,7 @@ const handleCheck = (option: Record<string, any>, checked: boolean) => {
352
382
 
353
383
  emitChange()
354
384
  dropdownRef.value?.updateDropdown()
385
+ refocusInput()
355
386
  }
356
387
 
357
388
  /** 全选 */
@@ -36,13 +36,18 @@ $root-name: multi-select;
36
36
  border-color: fn.use-var(color, primary);
37
37
  }
38
38
 
39
+ @include m.is(focus) {
40
+ border-color: fn.use-var(color, primary);
41
+ box-shadow: fn.use-var(focus-ring);
42
+ }
43
+
39
44
  @include m.e(ripple) {
40
45
  background-color: fn.use-var(color, primary, light-7);
41
46
  opacity: 0.5;
42
47
  }
43
48
 
44
49
  @include m.e(options) {
45
- max-height: 180px;
50
+ max-height: 240px;
46
51
  }
47
52
 
48
53
  @include m.e(options-wrap) {
@@ -79,6 +84,27 @@ $root-name: multi-select;
79
84
  @include m.flex($justify: flex-start, $align: center, $wrap: wrap);
80
85
  }
81
86
 
87
+ @include m.e(input) {
88
+ flex: 1;
89
+ min-width: 60px;
90
+ padding: 0;
91
+ margin: 0;
92
+ border: none;
93
+ outline: none;
94
+ background: transparent;
95
+ font: inherit;
96
+ color: fn.use-var(text-color, main);
97
+ cursor: text;
98
+
99
+ &::placeholder {
100
+ color: fn.use-var(text-color, placeholder);
101
+ }
102
+
103
+ &:disabled {
104
+ cursor: not-allowed;
105
+ }
106
+ }
107
+
82
108
  @include m.e(readonly-tags) {
83
109
  padding-left: 0;
84
110
  padding-right: 0;
@@ -104,9 +130,6 @@ $root-name: multi-select;
104
130
 
105
131
  @include m.e(panel) {
106
132
  @include m.size using ($size) {
107
- @include m.bem($root-name, content-filter) {
108
- padding: calc(fn.use-var(gap, $size) / 2) fn.use-var(gap, $size);
109
- }
110
133
  @include m.bem($root-name, content-header) {
111
134
  padding: calc(fn.use-var(gap, $size) / 2) calc(fn.use-var(gap, $size) * 2);
112
135
  border-bottom: fn.use-var(border);
@@ -2,7 +2,7 @@
2
2
  <u-dropdown
3
3
  v-if="!readonly"
4
4
  v-bind="$attrs"
5
- :class="[cls.b, cls.m(size), bem.is('disabled', disabled)]"
5
+ :class="[cls.b, cls.m(size), bem.is('disabled', disabled), bem.is('focus', dropdownVisible)]"
6
6
  trigger="click"
7
7
  :content-class="[cls.e('panel'), cls.em('panel', size), contentClass]"
8
8
  :content-style="contentStyle"
@@ -16,12 +16,12 @@
16
16
  >
17
17
  <template #trigger>
18
18
  <!-- 默认展示 -->
19
- <span :class="cls.e('placeholder')" v-show="!tags.length">
19
+ <span :class="cls.e('placeholder')" v-if="!filterable && !tags.length">
20
20
  {{ placeholder }}
21
21
  </span>
22
22
 
23
23
  <!-- 选择的数据项 -->
24
- <div v-if="tags.length" :class="cls.e('tags')">
24
+ <div v-if="tags.length || filterable" :class="cls.e('tags')">
25
25
  <u-tag
26
26
  v-for="(tag, index) in visibleTags"
27
27
  :key="tag[valueKey]"
@@ -31,6 +31,18 @@
31
31
  {{ tag[labelKey] }}
32
32
  </u-tag>
33
33
  <u-tag v-if="hiddenCount > 0">+{{ hiddenCount }}</u-tag>
34
+
35
+ <!-- 内嵌查询输入框:承载过滤 -->
36
+ <input
37
+ v-if="filterable"
38
+ ref="inputRef"
39
+ :class="cls.e('input')"
40
+ v-model="qs"
41
+ :placeholder="tags.length ? '' : placeholder"
42
+ :disabled="disabled"
43
+ @click.stop="handleInputClick"
44
+ @focus="handleInputFocus"
45
+ />
34
46
  </div>
35
47
  <!-- 清空 icon -->
36
48
  <transition name="zoom-in" mode="out-in">
@@ -59,14 +71,6 @@
59
71
  {{ allExpanded ? '收起全部' : '展开全部' }}
60
72
  </u-button>
61
73
  </div>
62
- <!-- 过滤器 -->
63
- <div v-if="filterable" :class="[cls.e('content-filter'), cls.m(size)]">
64
- <u-input placeholder="输入关键字进行过滤" v-model="qs">
65
- <template #suffix>
66
- <u-icon><Search /></u-icon>
67
- </template>
68
- </u-input>
69
- </div>
70
74
  <!-- 菜单列表 -->
71
75
  <u-tree
72
76
  v-bind="treeProps"
@@ -98,10 +102,10 @@
98
102
  <script lang="ts" setup>
99
103
  import { dfs, o } from '@cat-kit/core'
100
104
  import { useFormFallbackProps } from '@veltra/compositions'
101
- import { ArrowDown, Close, Search } from '@veltra/icons/normal'
105
+ import { ArrowDown, Close } from '@veltra/icons/normal'
102
106
  import { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'
103
107
  import { injectFormContext } from '@veltra/utils'
104
- import { computed, nextTick, shallowRef, watch } from 'vue'
108
+ import { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'
105
109
 
106
110
  import type { MultiTreeSelectProps, MultiTreeSelectEmits, TreeExposed } from '../../types'
107
111
  import type { DropdownExposed } from '../../types'
@@ -109,7 +113,6 @@ import { UButton } from '../button'
109
113
  import { UCheckbox } from '../checkbox'
110
114
  import { UDropdown } from '../dropdown'
111
115
  import { UIcon } from '../icon'
112
- import { UInput } from '../input'
113
116
  import { UTag } from '../tag'
114
117
  import { UTree } from '../tree'
115
118
  import type { TreeSlotsScope } from '../tree/di'
@@ -306,9 +309,26 @@ const hiddenCount = computed(() => {
306
309
  return 0
307
310
  })
308
311
 
312
+ const dropdownVisible = shallowRef(false)
313
+
314
+ const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
315
+
309
316
  const handleDropdownVisible = (visible: boolean) => {
317
+ dropdownVisible.value = visible
310
318
  if (!visible) {
311
319
  qs.value = ''
312
320
  }
313
321
  }
322
+
323
+ /**
324
+ * 拦截输入区域的点击(@click.stop):阻止 dropdown 的 trigger 开合切换,
325
+ * 保持面板展开以不中断输入。
326
+ */
327
+ const handleInputClick = () => {
328
+ if (!dropdownVisible.value) dropdownRef.value?.open()
329
+ }
330
+
331
+ const handleInputFocus = () => {
332
+ if (!dropdownVisible.value) dropdownRef.value?.open()
333
+ }
314
334
  </script>
@@ -22,6 +22,11 @@ $root-name: multi-tree-select;
22
22
  border-color: fn.use-var(color, primary);
23
23
  }
24
24
 
25
+ @include m.is(focus) {
26
+ border-color: fn.use-var(color, primary);
27
+ box-shadow: fn.use-var(focus-ring);
28
+ }
29
+
25
30
  @include m.size using ($size) {
26
31
  min-height: fn.use-var(form-component-height, $size);
27
32
  border-radius: fn.use-var(radius, $size);
@@ -58,16 +63,33 @@ $root-name: multi-tree-select;
58
63
  color: fn.use-var(text-color, placeholder);
59
64
  }
60
65
 
61
- @include m.e(filter) {
62
- margin-bottom: 4px;
63
- }
64
-
65
66
  @include m.e(tags) {
66
67
  flex-grow: 1;
67
68
  overflow: hidden;
68
69
  @include m.flex($justify: flex-start, $align: center, $wrap: wrap);
69
70
  }
70
71
 
72
+ @include m.e(input) {
73
+ flex: 1;
74
+ min-width: 60px;
75
+ padding: 0;
76
+ margin: 0;
77
+ border: none;
78
+ outline: none;
79
+ background: transparent;
80
+ font: inherit;
81
+ color: fn.use-var(text-color, main);
82
+ cursor: text;
83
+
84
+ &::placeholder {
85
+ color: fn.use-var(text-color, placeholder);
86
+ }
87
+
88
+ &:disabled {
89
+ cursor: not-allowed;
90
+ }
91
+ }
92
+
71
93
  @include m.e(readonly-tags) {
72
94
  padding-left: 0;
73
95
  padding-right: 0;
@@ -90,21 +112,18 @@ $root-name: multi-tree-select;
90
112
 
91
113
  @include m.e(panel) {
92
114
  @include m.size using ($size) {
93
- @include m.bem($root-name, content-filter) {
94
- padding: calc(fn.use-var(gap, $size) / 2) fn.use-var(gap, $size);
95
- }
96
115
  @include m.bem($root-name, content-header) {
97
116
  padding: calc(fn.use-var(gap, $size) / 2) fn.use-var(gap, $size);
98
117
  border-bottom: fn.use-var(border);
99
118
  }
100
119
 
101
120
  @include m.bem($root-name, content-tree) {
102
- padding: calc(fn.use-var(gap, $size) / 2) fn.use-var(gap, $size);
121
+ padding: fn.use-var(gap, $size);
103
122
  }
104
123
  }
105
124
  }
106
125
 
107
126
  @include m.e(content-tree) {
108
- max-height: 272px;
127
+ max-height: 240px;
109
128
  }
110
129
  }
@@ -0,0 +1,110 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createApp, h, nextTick, ref } from 'vue'
3
+
4
+ import USelect from '../select.vue'
5
+
6
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
7
+
8
+ function mountSelect(props: Record<string, unknown>) {
9
+ const host = document.createElement('div')
10
+ document.body.appendChild(host)
11
+
12
+ const model = ref(props.modelValue)
13
+ const app = createApp({
14
+ render() {
15
+ return h(USelect, {
16
+ ...props,
17
+ modelValue: model.value,
18
+ 'onUpdate:modelValue': (value: unknown) => {
19
+ model.value = value
20
+ }
21
+ })
22
+ }
23
+ })
24
+
25
+ app.mount(host)
26
+
27
+ return {
28
+ host,
29
+ model,
30
+ unmount() {
31
+ app.unmount()
32
+ host.remove()
33
+ }
34
+ }
35
+ }
36
+
37
+ function queryOptions() {
38
+ return [...document.body.querySelectorAll<HTMLElement>('.u-select__option')]
39
+ }
40
+
41
+ async function openDropdown(host: HTMLElement) {
42
+ const input = host.querySelector('input')!
43
+ // 与组件 capture 拦截一致:点击原生 input 打开面板并进入查询态
44
+ input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
45
+ await nextTick()
46
+ await nextTick()
47
+ }
48
+
49
+ async function typeQuery(host: HTMLElement, text: string) {
50
+ const input = host.querySelector('input')!
51
+ input.focus()
52
+ // 清空查询串(查询态下 model 绑定 queryString)
53
+ input.value = ''
54
+ input.dispatchEvent(
55
+ new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteContentBackward' })
56
+ )
57
+ await nextTick()
58
+
59
+ input.value = text
60
+ input.dispatchEvent(
61
+ new InputEvent('input', { bubbles: true, data: text, inputType: 'insertText' })
62
+ )
63
+ await nextTick()
64
+ // 本地过滤防抖 200ms
65
+ await sleep(250)
66
+ }
67
+
68
+ async function clickOption(option: HTMLElement) {
69
+ option.dispatchEvent(new MouseEvent('click', { bubbles: true }))
70
+ await nextTick()
71
+ await sleep(50)
72
+ }
73
+
74
+ describe('USelect', () => {
75
+ it('highlights the latest created option after consecutive creations', async () => {
76
+ const { host, model, unmount } = mountSelect({
77
+ filterable: true,
78
+ creatable: true,
79
+ options: [
80
+ { label: '选项0', value: '0' },
81
+ { label: '选项1', value: '1' }
82
+ ]
83
+ })
84
+
85
+ try {
86
+ // 创建并选择第一个选项
87
+ await openDropdown(host)
88
+ await typeQuery(host, '新A')
89
+ expect(queryOptions().some((el) => el.textContent === '新A')).toBe(true)
90
+ await clickOption(queryOptions().find((el) => el.textContent === '新A')!)
91
+ expect(model.value).toBe('新A')
92
+
93
+ // 创建并选择第二个选项
94
+ await openDropdown(host)
95
+ await typeQuery(host, '新B')
96
+ expect(queryOptions().some((el) => el.textContent === '新B')).toBe(true)
97
+ await clickOption(queryOptions().find((el) => el.textContent === '新B')!)
98
+ expect(model.value).toBe('新B')
99
+ expect(host.querySelector('input')!.value).toBe('新B')
100
+
101
+ // 重新展开,高亮应落在后创建的“新B”上,而非停留在“新A”
102
+ await openDropdown(host)
103
+ const selected = queryOptions().filter((el) => el.classList.contains('is-selected'))
104
+ expect(selected).toHaveLength(1)
105
+ expect(selected[0]!.textContent).toBe('新B')
106
+ } finally {
107
+ unmount()
108
+ }
109
+ })
110
+ })