@veltra/desktop 1.6.5 → 1.7.0

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 (112) hide show
  1. package/dist/components/action/style2.css +1 -1
  2. package/dist/components/auto-complete/style2.css +1 -1
  3. package/dist/components/badge/style2.css +1 -1
  4. package/dist/components/batch-edit/style2.css +1 -1
  5. package/dist/components/breadcrumb/style2.css +1 -1
  6. package/dist/components/button/style2.css +1 -1
  7. package/dist/components/calendar/style2.css +1 -1
  8. package/dist/components/card/style2.css +1 -1
  9. package/dist/components/cascade/style2.css +1 -1
  10. package/dist/components/check-tag/style2.css +1 -1
  11. package/dist/components/checkbox/style2.css +1 -1
  12. package/dist/components/checkbox-group/style2.css +1 -1
  13. package/dist/components/code-editor/style2.css +1 -1
  14. package/dist/components/collapse/style2.css +1 -1
  15. package/dist/components/condition-editor/style2.css +1 -1
  16. package/dist/components/contextmenu/style2.css +1 -1
  17. package/dist/components/date-panel/style2.css +1 -1
  18. package/dist/components/date-picker/style2.css +1 -1
  19. package/dist/components/date-range-picker/style2.css +1 -1
  20. package/dist/components/dialog/style2.css +1 -1
  21. package/dist/components/drawer/style2.css +1 -1
  22. package/dist/components/dropdown/style2.css +1 -1
  23. package/dist/components/dual-nav/style2.css +1 -1
  24. package/dist/components/empty/style2.css +1 -1
  25. package/dist/components/expression-editor/core/chip.js +1 -0
  26. package/dist/components/expression-editor/core/chip.js.map +1 -1
  27. package/dist/components/expression-editor/style2.css +1 -1
  28. package/dist/components/file-picker/style2.css +1 -1
  29. package/dist/components/file-viewer/style2.css +3 -3
  30. package/dist/components/float-button/style2.css +1 -1
  31. package/dist/components/form/style2.css +1 -1
  32. package/dist/components/form-item/style2.css +1 -1
  33. package/dist/components/gantt-chart/style2.css +1 -1
  34. package/dist/components/grid/style2.css +1 -1
  35. package/dist/components/grid-input/style2.css +1 -1
  36. package/dist/components/group-input/style2.css +1 -1
  37. package/dist/components/group-nav/style2.css +1 -1
  38. package/dist/components/icon/style2.css +1 -1
  39. package/dist/components/index.d.ts +2 -1
  40. package/dist/components/index.js +3 -1
  41. package/dist/components/index.js.map +1 -1
  42. package/dist/components/input/style2.css +1 -1
  43. package/dist/components/kbd/style2.css +1 -1
  44. package/dist/components/layout/style2.css +1 -1
  45. package/dist/components/list/style2.css +1 -1
  46. package/dist/components/loading/style2.css +1 -1
  47. package/dist/components/message/style2.css +1 -1
  48. package/dist/components/message-confirm/style2.css +1 -1
  49. package/dist/components/multi-select/style2.css +1 -1
  50. package/dist/components/multi-tree-select/style2.css +1 -1
  51. package/dist/components/nav/style2.css +1 -1
  52. package/dist/components/node-render/style2.css +1 -1
  53. package/dist/components/notification/style2.css +1 -1
  54. package/dist/components/number/style2.css +1 -1
  55. package/dist/components/number-input/style2.css +1 -1
  56. package/dist/components/number-range-input/style2.css +1 -1
  57. package/dist/components/paginator/style2.css +1 -1
  58. package/dist/components/palette/style2.css +1 -1
  59. package/dist/components/password-input/style2.css +1 -1
  60. package/dist/components/pop-confirm/style2.css +1 -1
  61. package/dist/components/progress/style2.css +1 -1
  62. package/dist/components/progress-nodes/style2.css +1 -1
  63. package/dist/components/radio/style2.css +1 -1
  64. package/dist/components/radio-group/style2.css +4 -13
  65. package/dist/components/rich-text-editor/style2.css +1 -1
  66. package/dist/components/scroll/style2.css +1 -1
  67. package/dist/components/segment/segment.js +98 -0
  68. package/dist/components/segment/segment.js.map +1 -0
  69. package/dist/components/segment/style.d.ts +1 -0
  70. package/dist/components/segment/style.js +1 -0
  71. package/dist/components/segment/style2.css +113 -0
  72. package/dist/components/select/select.js +12 -6
  73. package/dist/components/select/select.js.map +1 -1
  74. package/dist/components/select/style2.css +1 -1
  75. package/dist/components/slider/style2.css +1 -1
  76. package/dist/components/steps/style2.css +1 -1
  77. package/dist/components/switch/style2.css +1 -1
  78. package/dist/components/table/style2.css +1 -1
  79. package/dist/components/table-editor/style2.css +1 -1
  80. package/dist/components/tabs/style2.css +1 -1
  81. package/dist/components/tag/style2.css +1 -1
  82. package/dist/components/text/style2.css +1 -1
  83. package/dist/components/textarea/style2.css +1 -1
  84. package/dist/components/theme/style2.css +1 -1
  85. package/dist/components/theme/theme.js +2 -1
  86. package/dist/components/theme/theme.js.map +1 -1
  87. package/dist/components/tip/style2.css +1 -1
  88. package/dist/components/tree/style2.css +1 -1
  89. package/dist/components/tree-select/style2.css +1 -1
  90. package/dist/components/watermark/style2.css +1 -1
  91. package/dist/index.d.ts +20 -18
  92. package/dist/index.js +19 -18
  93. package/dist/style.js +1 -0
  94. package/dist/types/index.d.ts +2 -1
  95. package/dist/types/segment.d.ts +41 -0
  96. package/package.json +6 -6
  97. package/src/components/expression-editor/core/chip.ts +2 -0
  98. package/src/components/file-viewer/style.scss +11 -11
  99. package/src/components/index.ts +2 -0
  100. package/src/components/radio-group/__test__/radio-group.test.ts +113 -0
  101. package/src/components/radio-group/style.scss +4 -15
  102. package/src/components/segment/__test__/segment.test.ts +264 -0
  103. package/src/components/segment/index.ts +1 -0
  104. package/src/components/segment/segment.vue +77 -0
  105. package/src/components/segment/style.scss +92 -0
  106. package/src/components/segment/style.ts +1 -0
  107. package/src/components/select/__test__/select.test.ts +36 -0
  108. package/src/components/select/select.vue +18 -12
  109. package/src/components/theme/theme.vue +6 -1
  110. package/src/style.ts +1 -0
  111. package/src/types/index.ts +1 -0
  112. package/src/types/segment.ts +42 -0
@@ -695,23 +695,23 @@
695
695
  @include m.e(docx-body) {
696
696
  min-height: 100%;
697
697
  padding: clamp(22px, 3vw, 44px);
698
+ }
698
699
 
699
- :deep(.u-file-viewer__docx-doc) {
700
- margin: 0 auto;
701
- color: #222;
702
- background: #fff;
703
- box-shadow: var(--u-file-viewer-page-shadow);
704
- }
700
+ @include m.e(docx-doc) {
701
+ margin: 0 auto;
702
+ color: #222;
703
+ background: #fff;
704
+ box-shadow: var(--u-file-viewer-page-shadow);
705
705
  }
706
706
 
707
707
  &.is-modal &__docx-body {
708
708
  padding: clamp(24px, 4vw, 64px);
709
+ }
709
710
 
710
- :deep(.u-file-viewer__docx-doc) {
711
- box-shadow:
712
- 0 38px 110px rgba(0, 0, 0, 0.5),
713
- 0 0 0 1px rgba(255, 255, 255, 0.04);
714
- }
711
+ &.is-modal &__docx-doc {
712
+ box-shadow:
713
+ 0 38px 110px rgba(0, 0, 0, 0.5),
714
+ 0 0 0 1px rgba(255, 255, 255, 0.04);
715
715
  }
716
716
 
717
717
  @include m.e(sheet) {
@@ -149,3 +149,5 @@ export * from './palette'
149
149
  export * from './grid'
150
150
 
151
151
  export * from './badge'
152
+
153
+ export * from './segment'
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createApp, h, nextTick, ref } from 'vue'
3
+
4
+ import URadioGroup from '../radio-group.vue'
5
+
6
+ const items = [
7
+ { label: 'A', value: 'a' },
8
+ { label: 'B', value: 'b' }
9
+ ]
10
+
11
+ function mountRadioGroup(props: Record<string, unknown> = {}) {
12
+ const host = document.createElement('div')
13
+ document.body.appendChild(host)
14
+
15
+ const model = ref(props.modelValue)
16
+ const changes: unknown[] = []
17
+
18
+ const app = createApp({
19
+ render() {
20
+ return h(URadioGroup, {
21
+ ...props,
22
+ items: items as any,
23
+ modelValue: model.value,
24
+ 'onUpdate:modelValue': (value: any) => {
25
+ model.value = value
26
+ },
27
+ onChange: (item: any) => {
28
+ changes.push(item)
29
+ }
30
+ })
31
+ }
32
+ })
33
+
34
+ app.mount(host)
35
+
36
+ return {
37
+ host,
38
+ model,
39
+ changes,
40
+ unmount() {
41
+ app.unmount()
42
+ host.remove()
43
+ }
44
+ }
45
+ }
46
+
47
+ function queryRoot(host: HTMLElement) {
48
+ return host.querySelector<HTMLElement>('.u-radio-group')!
49
+ }
50
+
51
+ function queryRadios(host: HTMLElement) {
52
+ return [...host.querySelectorAll<HTMLElement>('.u-radio')]
53
+ }
54
+
55
+ function pick(host: HTMLElement, index: number) {
56
+ const input = queryRadios(host)[index]!.querySelector<HTMLInputElement>('input')!
57
+ input.checked = true
58
+ input.dispatchEvent(new Event('change', { bubbles: true }))
59
+ }
60
+
61
+ describe('RadioGroup', () => {
62
+ it('基础渲染:圆点元素存在且选项正确渲染', async () => {
63
+ const { host, unmount } = mountRadioGroup({ modelValue: 'a' })
64
+ await nextTick()
65
+
66
+ try {
67
+ const root = queryRoot(host)
68
+ expect(root).toBeTruthy()
69
+ expect(root.querySelector('.u-radio__button')).toBeTruthy()
70
+ const radios = queryRadios(host)
71
+ expect(radios.length).toBe(2)
72
+ expect(radios[0]!.classList.contains('is-checked')).toBe(true)
73
+ expect(radios[1]!.classList.contains('is-checked')).toBe(false)
74
+ } finally {
75
+ unmount()
76
+ }
77
+ })
78
+
79
+ it('禁用项呈禁用态且原生不可选中', async () => {
80
+ const { host, unmount } = mountRadioGroup({
81
+ modelValue: 'a',
82
+ disabledItem: (item: any) => item.value === 'b'
83
+ })
84
+ await nextTick()
85
+
86
+ try {
87
+ const radios = queryRadios(host)
88
+ expect(radios[1]!.classList.contains('is-disabled')).toBe(true)
89
+
90
+ const inputs = radios.map((radio) => radio.querySelector<HTMLInputElement>('input')!)
91
+ expect(inputs[0]!.disabled).toBe(false)
92
+ expect(inputs[1]!.disabled).toBe(true)
93
+ } finally {
94
+ unmount()
95
+ }
96
+ })
97
+
98
+ it('点击选项更新 modelValue 并 emit change(参数为 item)', async () => {
99
+ const { host, model, changes, unmount } = mountRadioGroup({ modelValue: 'a' })
100
+ await nextTick()
101
+
102
+ try {
103
+ pick(host, 1)
104
+ await nextTick()
105
+
106
+ expect(model.value).toBe('b')
107
+ expect(changes).toEqual([items[1]])
108
+ expect(queryRadios(host)[1]!.classList.contains('is-checked')).toBe(true)
109
+ } finally {
110
+ unmount()
111
+ }
112
+ })
113
+ })
@@ -8,6 +8,10 @@
8
8
 
9
9
  @include m.bem(radio) {
10
10
  margin: 0;
11
+
12
+ & + & {
13
+ margin-left: 0;
14
+ }
11
15
  }
12
16
 
13
17
  @include m.size using ($size) {
@@ -21,19 +25,4 @@
21
25
  display: flex;
22
26
  }
23
27
  }
24
-
25
- @include m.e(button-item) {
26
- margin: 0px !important;
27
- border-right: 0 !important;
28
- border-radius: 0 !important;
29
-
30
- &:first-child {
31
- border-radius: fn.use-var(radius, default) 0 0 fn.use-var(radius, default) !important;
32
- }
33
-
34
- &:last-child {
35
- border-right: fn.use-var(border-muted) !important;
36
- border-radius: 0 fn.use-var(radius, default) fn.use-var(radius, default) 0 !important;
37
- }
38
- }
39
28
  }
@@ -0,0 +1,264 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createApp, h, nextTick, ref } from 'vue'
3
+
4
+ import USegment from '../segment.vue'
5
+
6
+ const defaultItems = [
7
+ { label: 'Option A', value: 'a' },
8
+ { label: 'Option B', value: 'b' },
9
+ { label: 'Option C', value: 'c' }
10
+ ]
11
+
12
+ function mountSegment(props: Record<string, unknown> = {}) {
13
+ const host = document.createElement('div')
14
+ document.body.appendChild(host)
15
+
16
+ const model = ref(props.modelValue)
17
+ const changes: unknown[] = []
18
+
19
+ const app = createApp({
20
+ render() {
21
+ return h(USegment, {
22
+ items: defaultItems,
23
+ ...props,
24
+ modelValue: model.value,
25
+ 'onUpdate:modelValue': (value: any) => {
26
+ model.value = value
27
+ },
28
+ onChange: (item: any) => {
29
+ changes.push(item)
30
+ }
31
+ })
32
+ }
33
+ })
34
+
35
+ app.mount(host)
36
+
37
+ return {
38
+ host,
39
+ model,
40
+ changes,
41
+ unmount() {
42
+ app.unmount()
43
+ host.remove()
44
+ }
45
+ }
46
+ }
47
+
48
+ function queryRoot(host: HTMLElement) {
49
+ return host.querySelector<HTMLElement>('.u-segment')!
50
+ }
51
+
52
+ function queryItems(host: HTMLElement) {
53
+ return [...host.querySelectorAll<HTMLElement>('.u-segment__item')]
54
+ }
55
+
56
+ describe('USegment', () => {
57
+ it('基础渲染与默认选中态', async () => {
58
+ const { host, unmount } = mountSegment({ modelValue: 'b' })
59
+ await nextTick()
60
+
61
+ try {
62
+ const root = queryRoot(host)
63
+ expect(root).toBeTruthy()
64
+ expect(root.getAttribute('role')).toBe('radiogroup')
65
+
66
+ const items = queryItems(host)
67
+ expect(items.length).toBe(3)
68
+ expect(items[0]!.textContent).toBe('Option A')
69
+ expect(items[1]!.textContent).toBe('Option B')
70
+ expect(items[2]!.textContent).toBe('Option C')
71
+
72
+ expect(items[0]!.classList.contains('is-active')).toBe(false)
73
+ expect(items[1]!.classList.contains('is-active')).toBe(true)
74
+ expect(items[2]!.classList.contains('is-active')).toBe(false)
75
+
76
+ expect(items[1]!.getAttribute('aria-checked')).toBe('true')
77
+ expect(items[0]!.getAttribute('aria-checked')).toBe('false')
78
+ } finally {
79
+ unmount()
80
+ }
81
+ })
82
+
83
+ it('点击选项触发 modelValue 更新与 change 事件', async () => {
84
+ const { host, model, changes, unmount } = mountSegment({ modelValue: 'a' })
85
+ await nextTick()
86
+
87
+ try {
88
+ const items = queryItems(host)
89
+ expect(items[0]!.classList.contains('is-active')).toBe(true)
90
+
91
+ items[1]!.click()
92
+ await nextTick()
93
+
94
+ expect(model.value).toBe('b')
95
+ expect(changes).toEqual([defaultItems[1]])
96
+ expect(items[1]!.classList.contains('is-active')).toBe(true)
97
+ expect(items[0]!.classList.contains('is-active')).toBe(false)
98
+
99
+ // 点击已选中项不重复触发 change
100
+ items[1]!.click()
101
+ await nextTick()
102
+ expect(changes.length).toBe(1)
103
+ } finally {
104
+ unmount()
105
+ }
106
+ })
107
+
108
+ it('支持键盘 Enter 和 Space 触发选择', async () => {
109
+ const { host, model, changes, unmount } = mountSegment({ modelValue: 'a' })
110
+ await nextTick()
111
+
112
+ try {
113
+ const items = queryItems(host)
114
+
115
+ items[2]!.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
116
+ await nextTick()
117
+
118
+ expect(model.value).toBe('c')
119
+ expect(changes).toEqual([defaultItems[2]])
120
+
121
+ items[1]!.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }))
122
+ await nextTick()
123
+
124
+ expect(model.value).toBe('b')
125
+ expect(changes).toEqual([defaultItems[2], defaultItems[1]])
126
+ } finally {
127
+ unmount()
128
+ }
129
+ })
130
+
131
+ it('支持 custom labelKey 和 valueKey', async () => {
132
+ const customItems = [
133
+ { name: 'First', id: 1 },
134
+ { name: 'Second', id: 2 }
135
+ ]
136
+ const host = document.createElement('div')
137
+ document.body.appendChild(host)
138
+ const model = ref(1)
139
+ const changes: unknown[] = []
140
+
141
+ const app = createApp({
142
+ render() {
143
+ return h(USegment, {
144
+ items: customItems,
145
+ labelKey: 'name',
146
+ valueKey: 'id',
147
+ modelValue: model.value,
148
+ 'onUpdate:modelValue': (value: any) => {
149
+ model.value = value
150
+ },
151
+ onChange: (item: any) => {
152
+ changes.push(item)
153
+ }
154
+ })
155
+ }
156
+ })
157
+ app.mount(host)
158
+ await nextTick()
159
+
160
+ try {
161
+ const items = queryItems(host)
162
+ expect(items[0]!.textContent).toBe('First')
163
+ expect(items[1]!.textContent).toBe('Second')
164
+ expect(items[0]!.classList.contains('is-active')).toBe(true)
165
+
166
+ items[1]!.click()
167
+ await nextTick()
168
+
169
+ expect(model.value).toBe(2)
170
+ expect(changes).toEqual([customItems[1]])
171
+ } finally {
172
+ app.unmount()
173
+ host.remove()
174
+ }
175
+ })
176
+
177
+ it('整组 disabled 禁用控制', async () => {
178
+ const { host, model, changes, unmount } = mountSegment({ modelValue: 'a', disabled: true })
179
+ await nextTick()
180
+
181
+ try {
182
+ const root = queryRoot(host)
183
+ expect(root.classList.contains('is-disabled')).toBe(true)
184
+
185
+ const items = queryItems(host)
186
+ expect(items[0]!.classList.contains('is-disabled')).toBe(true)
187
+ expect(items[1]!.classList.contains('is-disabled')).toBe(true)
188
+
189
+ items[1]!.click()
190
+ await nextTick()
191
+
192
+ expect(model.value).toBe('a')
193
+ expect(changes.length).toBe(0)
194
+ } finally {
195
+ unmount()
196
+ }
197
+ })
198
+
199
+ it('disabledItem 单项禁用控制', async () => {
200
+ const { host, model, changes, unmount } = mountSegment({
201
+ modelValue: 'a',
202
+ disabledItem: (item: any) => item.value === 'b'
203
+ })
204
+ await nextTick()
205
+
206
+ try {
207
+ const items = queryItems(host)
208
+ expect(items[0]!.classList.contains('is-disabled')).toBe(false)
209
+ expect(items[1]!.classList.contains('is-disabled')).toBe(true)
210
+ expect(items[2]!.classList.contains('is-disabled')).toBe(false)
211
+
212
+ // 点击禁用项无反应
213
+ items[1]!.click()
214
+ await nextTick()
215
+ expect(model.value).toBe('a')
216
+ expect(changes.length).toBe(0)
217
+
218
+ // 点击非禁用项正常切换
219
+ items[2]!.click()
220
+ await nextTick()
221
+ expect(model.value).toBe('c')
222
+ expect(changes).toEqual([defaultItems[2]])
223
+ } finally {
224
+ unmount()
225
+ }
226
+ })
227
+
228
+ it('block 属性与 size 样式类名绑定', async () => {
229
+ const { host, unmount } = mountSegment({ modelValue: 'a', block: true, size: 'small' })
230
+ await nextTick()
231
+
232
+ try {
233
+ const root = queryRoot(host)
234
+ expect(root.classList.contains('is-block')).toBe(true)
235
+ expect(root.classList.contains('u-segment--small')).toBe(true)
236
+ } finally {
237
+ unmount()
238
+ }
239
+ })
240
+
241
+ it('readonly 模式回退展示文本', async () => {
242
+ const { host, unmount } = mountSegment({ modelValue: 'b', readonly: true })
243
+ await nextTick()
244
+
245
+ try {
246
+ expect(host.querySelector('.u-segment')).toBeNull()
247
+ expect(host.textContent?.trim()).toBe('Option B')
248
+ } finally {
249
+ unmount()
250
+ }
251
+ })
252
+
253
+ it('readonly 模式未匹配到值时展示 FORM_EMPTY_CONTENT(-)', async () => {
254
+ const { host, unmount } = mountSegment({ modelValue: 'non-existing', readonly: true })
255
+ await nextTick()
256
+
257
+ try {
258
+ expect(host.querySelector('.u-segment')).toBeNull()
259
+ expect(host.textContent?.trim()).toBe('-')
260
+ } finally {
261
+ unmount()
262
+ }
263
+ })
264
+ })
@@ -0,0 +1 @@
1
+ export { default as USegment } from './segment.vue'
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <div
3
+ v-if="!readonly"
4
+ v-bind="$attrs"
5
+ :class="[cls.b, bem.is('block', block), bem.is('disabled', disabled), cls.m(size)]"
6
+ role="radiogroup"
7
+ >
8
+ <div
9
+ v-for="item of items"
10
+ :key="item[valueKey]"
11
+ :class="[
12
+ cls.e('item'),
13
+ bem.is('active', item[valueKey] === model),
14
+ bem.is('disabled', disabled || Boolean(disabledItem?.(item)))
15
+ ]"
16
+ role="radio"
17
+ :aria-checked="item[valueKey] === model"
18
+ :aria-disabled="disabled || Boolean(disabledItem?.(item))"
19
+ tabindex="0"
20
+ @click="handleSelect(item)"
21
+ @keydown.enter.prevent="handleSelect(item)"
22
+ @keydown.space.prevent="handleSelect(item)"
23
+ >
24
+ <slot name="item" :item="item" :active="item[valueKey] === model">
25
+ <span :class="cls.e('item-label')">{{ item[labelKey] }}</span>
26
+ </slot>
27
+ </div>
28
+ </div>
29
+
30
+ <template v-else>
31
+ {{ items.find((item) => item[valueKey] === model)?.[labelKey] || FORM_EMPTY_CONTENT }}
32
+ </template>
33
+ </template>
34
+
35
+ <script lang="ts" setup>
36
+ import { useFormFallbackProps } from '@veltra/compositions'
37
+ import { bem, fieldKey, FORM_EMPTY_CONTENT, injectFormContext } from '@veltra/utils'
38
+ import { computed } from 'vue'
39
+
40
+ import type { SegmentEmits, SegmentItem, SegmentProps } from '../../types'
41
+
42
+ defineOptions({ name: 'USegment', inheritAttrs: false })
43
+
44
+ const props = withDefaults(defineProps<SegmentProps>(), {
45
+ items: () => [],
46
+ labelKey: 'label',
47
+ valueKey: 'value',
48
+ disabled: undefined,
49
+ readonly: undefined,
50
+ block: false
51
+ })
52
+
53
+ const model = defineModel<any>()
54
+
55
+ const emit = defineEmits<SegmentEmits>()
56
+
57
+ const { formProps } = injectFormContext()
58
+
59
+ const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {
60
+ size: 'default',
61
+ disabled: false,
62
+ readonly: false
63
+ })
64
+
65
+ const cls = bem('segment')
66
+
67
+ const labelKey = computed(() => fieldKey(props.labelKey, 'label'))
68
+ const valueKey = computed(() => fieldKey(props.valueKey, 'value'))
69
+
70
+ const handleSelect = (item: SegmentItem) => {
71
+ if (disabled.value || props.disabledItem?.(item)) return
72
+ const val = item[valueKey.value]
73
+ if (model.value === val) return
74
+ model.value = val
75
+ emit('change', item)
76
+ }
77
+ </script>
@@ -0,0 +1,92 @@
1
+ @use 'pkg:@veltra/styles/mixins' as m;
2
+ @use 'pkg:@veltra/styles/functions' as fn;
3
+ @use 'pkg:@veltra/styles/vars';
4
+
5
+ @include m.b(segment) {
6
+ vertical-align: middle;
7
+ @include m.flex($display: inline-flex, $align: center);
8
+ gap: 2px;
9
+ padding: 2px;
10
+ background-color: fn.use-var(bg-color, bottom);
11
+ box-sizing: border-box;
12
+
13
+ @include m.size using ($size) {
14
+ border-radius: fn.use-var(radius, $size);
15
+
16
+ @include m.bem(segment, item) {
17
+ height: calc(fn.use-var(form-component-height, $size) - 4px);
18
+ padding: 0 calc(fn.use-var(gap, $size) * 1.5);
19
+ border-radius: calc(fn.use-var(radius, $size) - 2px);
20
+ font-size: fn.use-var(font-size-main, $size);
21
+ }
22
+ }
23
+
24
+ @include m.e(item) {
25
+ margin: 0;
26
+ display: inline-flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ background-color: transparent;
30
+ box-shadow: none;
31
+ font-weight: 500;
32
+ color: fn.use-var(text-color, second);
33
+ cursor: pointer;
34
+ user-select: none;
35
+ outline: none;
36
+ transition:
37
+ color fn.use-var(transition, fast) fn.use-var(transition, ease),
38
+ background-color fn.use-var(transition, fast) fn.use-var(transition, ease),
39
+ box-shadow fn.use-var(transition, fast) fn.use-var(transition, ease);
40
+
41
+ &:hover:not(.is-active):not(.is-disabled) {
42
+ color: fn.use-var(text-color, title);
43
+ background-color: fn.color-a(color, 5, primary);
44
+ }
45
+
46
+ @include m.is(active) {
47
+ color: fn.use-var(color, primary);
48
+ background-color: fn.use-var(bg-color, top);
49
+ box-shadow: fn.use-var(shadow, sm);
50
+ font-weight: 600;
51
+ }
52
+
53
+ @include m.is(disabled) {
54
+ opacity: 0.5;
55
+ cursor: not-allowed;
56
+ color: fn.use-var(text-color, disabled);
57
+ }
58
+
59
+ &:focus-visible:not(.is-disabled) {
60
+ box-shadow: fn.use-var(focus-ring);
61
+ }
62
+ }
63
+
64
+ @include m.e(item-label) {
65
+ @include m.ellipsis;
66
+ display: inline-flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ }
70
+
71
+ @include m.is(block) {
72
+ display: flex;
73
+ width: 100%;
74
+
75
+ @include m.bem(segment, item) {
76
+ flex: 1 1 0%;
77
+ width: 0;
78
+ min-width: 0;
79
+ display: inline-flex;
80
+ justify-content: center;
81
+ }
82
+ }
83
+
84
+ @include m.is(disabled) {
85
+ opacity: 0.7;
86
+ cursor: not-allowed;
87
+
88
+ @include m.bem(segment, item) {
89
+ cursor: not-allowed;
90
+ }
91
+ }
92
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -117,6 +117,42 @@ describe('USelect', () => {
117
117
  }
118
118
  })
119
119
 
120
+ it('keeps the filtered list while the panel is closing after a select', async () => {
121
+ const { host, model, unmount } = mountSelect({
122
+ filterable: true,
123
+ options: [
124
+ { label: '北京', value: 'beijing' },
125
+ { label: '上海', value: 'shanghai' },
126
+ { label: '广州', value: 'guangzhou' }
127
+ ]
128
+ })
129
+
130
+ try {
131
+ await openDropdown(host)
132
+ await typeQuery(host, '上海')
133
+ expect(queryOptions().map((el) => el.textContent)).toEqual(['上海'])
134
+
135
+ // 选择后关闭动画期间:面板仍在 DOM 中,列表必须保持过滤态,
136
+ // 不能瞬间恢复全量导致面板突然变长闪烁
137
+ queryOptions()[0]!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
138
+ await nextTick()
139
+ expect(model.value).toBe('shanghai')
140
+ const closingOptions = queryOptions()
141
+ expect(closingOptions.length).toBeGreaterThan(0)
142
+ expect(closingOptions.map((el) => el.textContent)).toEqual(['上海'])
143
+
144
+ // 动画兜底结束后面板卸载
145
+ await sleep(150)
146
+ expect(queryOptions()).toHaveLength(0)
147
+
148
+ // 重新展开:列表已恢复为完整选项
149
+ await openDropdown(host)
150
+ expect(queryOptions()).toHaveLength(3)
151
+ } finally {
152
+ unmount()
153
+ }
154
+ })
155
+
120
156
  it('emits update:text with option label when modelValue echoes against options', async () => {
121
157
  const { host, texts, unmount } = mountSelect({
122
158
  modelValue: 'beijing',