@veltra/desktop 1.6.6 → 1.7.1

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 (111) 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 +6 -6
  24. package/dist/components/empty/style2.css +1 -1
  25. package/dist/components/expression-editor/style2.css +1 -1
  26. package/dist/components/file-picker/style2.css +1 -1
  27. package/dist/components/file-viewer/style2.css +1 -1
  28. package/dist/components/float-button/style2.css +1 -1
  29. package/dist/components/form/style2.css +1 -1
  30. package/dist/components/form-item/style2.css +1 -1
  31. package/dist/components/gantt-chart/style2.css +1 -1
  32. package/dist/components/grid/style2.css +1 -1
  33. package/dist/components/grid-input/style2.css +1 -1
  34. package/dist/components/group-input/style2.css +1 -1
  35. package/dist/components/group-nav/style2.css +2 -2
  36. package/dist/components/icon/style2.css +1 -1
  37. package/dist/components/index.d.ts +2 -1
  38. package/dist/components/index.js +3 -1
  39. package/dist/components/index.js.map +1 -1
  40. package/dist/components/input/style2.css +1 -1
  41. package/dist/components/kbd/style2.css +1 -1
  42. package/dist/components/layout/style2.css +1 -1
  43. package/dist/components/list/style2.css +1 -1
  44. package/dist/components/loading/style2.css +1 -1
  45. package/dist/components/message/style2.css +1 -1
  46. package/dist/components/message-confirm/style2.css +1 -1
  47. package/dist/components/multi-select/style2.css +1 -1
  48. package/dist/components/multi-tree-select/style2.css +1 -1
  49. package/dist/components/nav/style2.css +8 -8
  50. package/dist/components/node-render/style2.css +1 -1
  51. package/dist/components/notification/style2.css +1 -1
  52. package/dist/components/number/style2.css +1 -1
  53. package/dist/components/number-input/style2.css +1 -1
  54. package/dist/components/number-range-input/style2.css +1 -1
  55. package/dist/components/paginator/style2.css +1 -1
  56. package/dist/components/palette/style2.css +1 -1
  57. package/dist/components/password-input/style2.css +1 -1
  58. package/dist/components/pop-confirm/style2.css +1 -1
  59. package/dist/components/progress/style2.css +1 -1
  60. package/dist/components/progress-nodes/style2.css +1 -1
  61. package/dist/components/radio/style2.css +1 -1
  62. package/dist/components/radio-group/style2.css +4 -13
  63. package/dist/components/rich-text-editor/style2.css +1 -1
  64. package/dist/components/scroll/style2.css +1 -1
  65. package/dist/components/segment/segment.js +98 -0
  66. package/dist/components/segment/segment.js.map +1 -0
  67. package/dist/components/segment/style.d.ts +1 -0
  68. package/dist/components/segment/style.js +1 -0
  69. package/dist/components/segment/style2.css +113 -0
  70. package/dist/components/select/select.js +12 -6
  71. package/dist/components/select/select.js.map +1 -1
  72. package/dist/components/select/style2.css +1 -1
  73. package/dist/components/slider/style2.css +1 -1
  74. package/dist/components/steps/style2.css +1 -1
  75. package/dist/components/switch/style2.css +1 -1
  76. package/dist/components/table/style2.css +1 -1
  77. package/dist/components/table-editor/style2.css +1 -1
  78. package/dist/components/tabs/style2.css +1 -1
  79. package/dist/components/tag/style2.css +1 -1
  80. package/dist/components/text/style2.css +1 -1
  81. package/dist/components/textarea/style2.css +1 -1
  82. package/dist/components/theme/style2.css +1 -1
  83. package/dist/components/theme/theme.js +2 -1
  84. package/dist/components/theme/theme.js.map +1 -1
  85. package/dist/components/tip/style2.css +1 -1
  86. package/dist/components/tree/style2.css +1 -1
  87. package/dist/components/tree-select/style2.css +1 -1
  88. package/dist/components/watermark/style2.css +1 -1
  89. package/dist/index.d.ts +20 -18
  90. package/dist/index.js +19 -18
  91. package/dist/style.js +1 -0
  92. package/dist/types/index.d.ts +2 -1
  93. package/dist/types/segment.d.ts +41 -0
  94. package/package.json +6 -6
  95. package/src/components/dual-nav/style.scss +5 -5
  96. package/src/components/group-nav/style.scss +1 -1
  97. package/src/components/index.ts +2 -0
  98. package/src/components/nav/style.scss +4 -4
  99. package/src/components/radio-group/__test__/radio-group.test.ts +113 -0
  100. package/src/components/radio-group/style.scss +4 -15
  101. package/src/components/segment/__test__/segment.test.ts +264 -0
  102. package/src/components/segment/index.ts +1 -0
  103. package/src/components/segment/segment.vue +77 -0
  104. package/src/components/segment/style.scss +92 -0
  105. package/src/components/segment/style.ts +1 -0
  106. package/src/components/select/__test__/select.test.ts +36 -0
  107. package/src/components/select/select.vue +18 -12
  108. package/src/components/theme/theme.vue +6 -1
  109. package/src/style.ts +1 -0
  110. package/src/types/index.ts +1 -0
  111. package/src/types/segment.ts +42 -0
@@ -0,0 +1,41 @@
1
+ import { DeconstructValue, FormComponentProps } from "@veltra/utils";
2
+ //#region src/types/segment.d.ts
3
+ /** 分段单选选项 */
4
+ type SegmentItem = Record<string, any>;
5
+ /** 分段单选组件属性 */
6
+ interface SegmentProps extends FormComponentProps {
7
+ /** 绑定值 */
8
+ modelValue?: any;
9
+ /** 选项列表 */
10
+ items: SegmentItem[];
11
+ /**
12
+ * 选项值 key
13
+ * @default 'value'
14
+ */
15
+ valueKey?: string;
16
+ /**
17
+ * 选项标签 key
18
+ * @default 'label'
19
+ */
20
+ labelKey?: string;
21
+ /** 是否禁用整组 */
22
+ disabled?: boolean;
23
+ /** 禁用的选项判断函数 */
24
+ disabledItem?: (item: SegmentItem) => boolean;
25
+ /** 是否撑满容器宽度 */
26
+ block?: boolean;
27
+ }
28
+ /** 分段单选组件事件 */
29
+ interface SegmentEmits {
30
+ /** 绑定值更新 */
31
+ (e: 'update:modelValue', modelValue: any): void;
32
+ /** 选中项切换事件 */
33
+ (e: 'change', item: SegmentItem): void;
34
+ }
35
+ /** 分段单选组件暴露的属性和方法(组件内部使用) */
36
+ interface _SegmentExposed {}
37
+ /** 分段单选组件暴露的属性和方法(组件外部使用, 引用的值会被自动解构) */
38
+ type SegmentExposed = DeconstructValue<_SegmentExposed>;
39
+ //#endregion
40
+ export { SegmentEmits, SegmentExposed, SegmentItem, SegmentProps, _SegmentExposed };
41
+ //# sourceMappingURL=segment.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/desktop",
3
- "version": "1.6.6",
3
+ "version": "1.7.1",
4
4
  "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cabinet-fe/ultra-ui/issues"
@@ -80,12 +80,12 @@
80
80
  "peerDependencies": {
81
81
  "@cat-kit/core": ">=1.1.8",
82
82
  "@cat-kit/fe": ">=1.1.8",
83
- "@veltra/compositions": "^1.6.6",
84
- "@veltra/directives": "^1.6.6",
83
+ "@veltra/compositions": "^1.7.1",
84
+ "@veltra/directives": "^1.7.1",
85
85
  "@veltra/icons": "^1.5.0",
86
- "@veltra/sheet-core": "^2.5.0",
87
- "@veltra/styles": "^1.6.6",
88
- "@veltra/utils": "^1.6.6",
86
+ "@veltra/sheet-core": "^2.5.2",
87
+ "@veltra/styles": "^1.7.1",
88
+ "@veltra/utils": "^1.7.1",
89
89
  "vue": ">=3.5.41"
90
90
  },
91
91
  "peerDependenciesMeta": {
@@ -20,7 +20,7 @@ $root-name: dual-nav;
20
20
  align-items: center;
21
21
  gap: 4px;
22
22
  padding: 8px 6px;
23
- background-color: fn.use-var(bg-color, middle);
23
+ background-color: var(--u-nav-rail-bg);
24
24
  }
25
25
 
26
26
  @include m.m(labeled) {
@@ -76,14 +76,14 @@ $root-name: dual-nav;
76
76
  font-size: fn.use-var(font-size, default);
77
77
  font-weight: 600;
78
78
  line-height: 1.4;
79
- color: fn.use-var(text-color, title);
79
+ color: var(--u-nav-strong-color);
80
80
  }
81
81
 
82
82
  @include m.e(description) {
83
83
  margin: 0;
84
84
  font-size: fn.use-var(font-size-assist, default);
85
85
  line-height: 1.5;
86
- color: fn.use-var(text-color, second);
86
+ color: var(--u-nav-second-color);
87
87
  }
88
88
 
89
89
  @include m.e(toggle-expand) {
@@ -97,7 +97,7 @@ $root-name: dual-nav;
97
97
  border: none;
98
98
  border-radius: fn.use-var(radius, default);
99
99
  background: transparent;
100
- color: fn.use-var(text-color, second);
100
+ color: var(--u-nav-second-color);
101
101
  cursor: pointer;
102
102
  transition:
103
103
  background-color 0.15s ease,
@@ -133,7 +133,7 @@ $root-name: dual-nav;
133
133
  border: none;
134
134
  border-radius: calc(fn.use-var(radius, default) + 2px);
135
135
  background: transparent;
136
- color: fn.use-var(text-color, second);
136
+ color: var(--u-nav-second-color);
137
137
  cursor: pointer;
138
138
  transition:
139
139
  background-color 0.15s ease,
@@ -40,7 +40,7 @@
40
40
  font-weight: 500;
41
41
  line-height: 1.4;
42
42
  letter-spacing: 0.02em;
43
- color: fn.use-var(text-color, second);
43
+ color: var(--u-nav-second-color);
44
44
  }
45
45
 
46
46
  @include m.e(group-list) {
@@ -149,3 +149,5 @@ export * from './palette'
149
149
  export * from './grid'
150
150
 
151
151
  export * from './badge'
152
+
153
+ export * from './segment'
@@ -33,7 +33,7 @@ $collapsed-nav: collapsed-nav;
33
33
  }
34
34
 
35
35
  @include m.is(expanded, branch-active) {
36
- color: fn.use-var(text-color, title);
36
+ color: var(--u-nav-strong-color);
37
37
  }
38
38
 
39
39
  @include m.is(active) {
@@ -121,7 +121,7 @@ $collapsed-nav: collapsed-nav;
121
121
  }
122
122
 
123
123
  @include m.e(sub-expand, icon) {
124
- color: fn.use-var(text-color, second);
124
+ color: var(--u-nav-second-color);
125
125
  display: inline-flex;
126
126
  justify-content: center;
127
127
  align-items: center;
@@ -146,7 +146,7 @@ $collapsed-nav: collapsed-nav;
146
146
  overflow: hidden;
147
147
  margin: 2px 0 0 12px;
148
148
  padding: 2px 0 2px 8px;
149
- border-left: 1px solid fn.color-a(border, 52, color);
149
+ border-left: 1px solid var(--u-nav-sub-border-color);
150
150
 
151
151
  @include m.is(has-active) {
152
152
  border-left-color: fn.use-var(color, primary);
@@ -193,7 +193,7 @@ $collapsed-nav: collapsed-nav;
193
193
  }
194
194
 
195
195
  @include m.e(icon) {
196
- color: fn.use-var(text-color, second);
196
+ color: var(--u-nav-second-color);
197
197
  object-fit: contain;
198
198
  }
199
199
 
@@ -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>