niuma-ui 1.0.2 → 1.1.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 (85) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/RsDatePicker.spec.ts +21 -0
  4. package/src/__tests__/RsDialog.spec.ts +61 -0
  5. package/src/__tests__/RsForm.spec.ts +5 -5
  6. package/src/__tests__/RsFormItem.spec.ts +478 -0
  7. package/src/__tests__/RsFormList.spec.ts +109 -0
  8. package/src/__tests__/RsInput.spec.ts +93 -0
  9. package/src/__tests__/RsInputNumber.spec.ts +33 -0
  10. package/src/__tests__/RsSelect.spec.ts +423 -1
  11. package/src/__tests__/RsSwitch.spec.ts +30 -0
  12. package/src/__tests__/RsTimePicker.spec.ts +9 -0
  13. package/src/__tests__/RsTooltip.spec.ts +43 -0
  14. package/src/__tests__/css-token.spec.ts +51 -0
  15. package/src/__tests__/date-picker-utils.spec.ts +49 -0
  16. package/src/__tests__/dialog-utils.spec.ts +43 -0
  17. package/src/__tests__/form-path.spec.ts +46 -0
  18. package/src/__tests__/form-rules.spec.ts +63 -0
  19. package/src/__tests__/theme.spec.ts +2 -0
  20. package/src/components/RsAlert.vue +1 -1
  21. package/src/components/RsAvatar.vue +1 -1
  22. package/src/components/RsBadge.vue +1 -1
  23. package/src/components/RsBreadcrumb.vue +1 -1
  24. package/src/components/RsButton.vue +4 -4
  25. package/src/components/RsCalendarGrid.vue +3 -3
  26. package/src/components/RsCard.vue +4 -4
  27. package/src/components/RsCodeBlock.vue +4 -4
  28. package/src/components/RsCodeEditor.vue +8 -8
  29. package/src/components/RsConfirmDialog.vue +2 -2
  30. package/src/components/RsContextMenu.vue +6 -4
  31. package/src/components/RsDatePicker.vue +75 -31
  32. package/src/components/RsDateTimePicker.vue +9 -1
  33. package/src/components/RsDescriptions.vue +2 -2
  34. package/src/components/RsDialog.vue +26 -16
  35. package/src/components/RsDrawer.vue +1 -1
  36. package/src/components/RsDropdown.vue +1 -1
  37. package/src/components/RsDynamicTags.vue +18 -4
  38. package/src/components/RsEmpty.vue +1 -1
  39. package/src/components/RsForm.vue +68 -1
  40. package/src/components/RsFormItem.vue +423 -0
  41. package/src/components/RsFormItemControl.vue +21 -0
  42. package/src/components/RsFormList.vue +117 -0
  43. package/src/components/RsInput.vue +411 -18
  44. package/src/components/RsInputNumber.vue +13 -4
  45. package/src/components/RsLink.vue +1 -1
  46. package/src/components/RsMarkdown.vue +5 -5
  47. package/src/components/RsMenu.vue +2 -2
  48. package/src/components/RsMonacoEditor.vue +9 -3
  49. package/src/components/RsPagination.vue +1 -1
  50. package/src/components/RsProseEditor.vue +3 -3
  51. package/src/components/RsSelect.vue +332 -611
  52. package/src/components/RsSidebarGroup.vue +1 -1
  53. package/src/components/RsSplitPane.vue +22 -1
  54. package/src/components/RsStatCard.vue +2 -2
  55. package/src/components/RsSteps.vue +2 -2
  56. package/src/components/RsSwitch.vue +20 -7
  57. package/src/components/RsTabs.vue +9 -9
  58. package/src/components/RsTag.vue +1 -1
  59. package/src/components/RsTerminal.vue +58 -19
  60. package/src/components/RsTimePicker.vue +28 -4
  61. package/src/components/RsTimePickerColumns.vue +4 -4
  62. package/src/components/RsToaster.vue +7 -7
  63. package/src/components/RsTooltip.vue +10 -1
  64. package/src/components/RsTooltipProvider.vue +11 -1
  65. package/src/components/RsTree.vue +6 -6
  66. package/src/components/RsUpload.vue +2 -2
  67. package/src/components/date-picker-utils.ts +113 -2
  68. package/src/components/dialog-utils.ts +37 -2
  69. package/src/components/form-item-control.ts +55 -0
  70. package/src/components/form-path.ts +92 -0
  71. package/src/components/form-rules.ts +64 -10
  72. package/src/components/form-utils.ts +94 -2
  73. package/src/components/select-utils.ts +272 -12
  74. package/src/components/table/RsTableHeaderFilter.vue +1 -1
  75. package/src/components/table/rs-table.css +1 -1
  76. package/src/components/use-rs-select.ts +436 -0
  77. package/src/composables/useRsConfig.ts +10 -3
  78. package/src/composables/useRsI18n.ts +16 -5
  79. package/src/index.ts +69 -1
  80. package/src/locale/interpolate.ts +32 -0
  81. package/src/locale/messages.ts +10 -0
  82. package/src/styles.css +68 -12
  83. package/src/theme/brand.example.css +4 -1
  84. package/src/theme/css-token.ts +107 -0
  85. package/src/theme/types.ts +1 -0
@@ -0,0 +1,109 @@
1
+ import { defineComponent, ref } from 'vue'
2
+ import { afterEach, describe, expect, it } from 'vitest'
3
+ import { flushPromises, mount } from '@vue/test-utils'
4
+ import RsForm from '../components/RsForm.vue'
5
+ import RsFormItem from '../components/RsFormItem.vue'
6
+ import RsFormList from '../components/RsFormList.vue'
7
+ import RsInput from '../components/RsInput.vue'
8
+
9
+ describe('RsFormList', () => {
10
+ afterEach(() => {
11
+ document.body.innerHTML = ''
12
+ })
13
+
14
+ it('adds and removes rows on Form.model', async () => {
15
+ const Host = defineComponent({
16
+ components: { RsForm, RsFormItem, RsFormList, RsInput },
17
+ setup() {
18
+ const formRef = ref<InstanceType<typeof RsForm> | null>(null)
19
+ const model = ref<{ users: Array<{ name: string }> }>({ users: [] })
20
+ return { formRef, model }
21
+ },
22
+ template: `
23
+ <RsForm ref="formRef" :model="model">
24
+ <RsFormList name="users">
25
+ <template #default="{ fields, add, remove }">
26
+ <div v-for="field in fields" :key="field.key" class="row">
27
+ <RsFormItem :name="[field.name, 'name']" required>
28
+ <RsInput />
29
+ </RsFormItem>
30
+ <button type="button" class="remove" @click="remove(field.index)">x</button>
31
+ </div>
32
+ <button type="button" class="add" @click="add({ name: '' })">add</button>
33
+ </template>
34
+ </RsFormList>
35
+ </RsForm>
36
+ `,
37
+ })
38
+ const wrapper = mount(Host)
39
+ await flushPromises()
40
+ await wrapper.find('.add').trigger('click')
41
+ await flushPromises()
42
+ expect(wrapper.vm.model.users).toEqual([{ name: '' }])
43
+
44
+ const invalid = await wrapper.vm.formRef!.validate()
45
+ expect(invalid.valid).toBe(false)
46
+ expect(invalid.errors['users.0.name']).toBe('此项为必填')
47
+
48
+ wrapper.find('input').setValue('Ada')
49
+ await flushPromises()
50
+ const ok = await wrapper.vm.formRef!.validate()
51
+ expect(ok.valid).toBe(true)
52
+ expect(wrapper.vm.model.users[0]?.name).toBe('Ada')
53
+
54
+ await wrapper.find('.remove').trigger('click')
55
+ await flushPromises()
56
+ expect(wrapper.vm.model.users).toEqual([])
57
+ wrapper.unmount()
58
+ })
59
+
60
+ it('resolves list-relative dependencies and getFieldValue', async () => {
61
+ const Host = defineComponent({
62
+ components: { RsForm, RsFormItem, RsFormList, RsInput },
63
+ setup() {
64
+ const formRef = ref<InstanceType<typeof RsForm> | null>(null)
65
+ const model = ref({
66
+ users: [{ password: 'aa', confirm: 'bb' }],
67
+ })
68
+ return { formRef, model }
69
+ },
70
+ template: `
71
+ <RsForm ref="formRef" :model="model">
72
+ <RsFormList name="users">
73
+ <template #default="{ fields }">
74
+ <div v-for="field in fields" :key="field.key">
75
+ <RsFormItem :name="[field.name, 'password']">
76
+ <RsInput />
77
+ </RsFormItem>
78
+ <RsFormItem
79
+ :name="[field.name, 'confirm']"
80
+ :dependencies="[[field.name, 'password']]"
81
+ :rules="[{
82
+ validator(value, ctx) {
83
+ if (String(value) !== String(ctx.getFieldValue([field.name, 'password']))) {
84
+ return '两次密码不一致'
85
+ }
86
+ return true
87
+ }
88
+ }]"
89
+ >
90
+ <RsInput />
91
+ </RsFormItem>
92
+ </div>
93
+ </template>
94
+ </RsFormList>
95
+ </RsForm>
96
+ `,
97
+ })
98
+ const wrapper = mount(Host)
99
+ await flushPromises()
100
+ const invalid = await wrapper.vm.formRef!.validate()
101
+ expect(invalid.valid).toBe(false)
102
+ expect(invalid.errors['users.0.confirm']).toBe('两次密码不一致')
103
+
104
+ wrapper.vm.model.users[0]!.password = 'bb'
105
+ await flushPromises()
106
+ expect(wrapper.findAll('.rs-form-item__error').length).toBe(0)
107
+ wrapper.unmount()
108
+ })
109
+ })
@@ -280,4 +280,97 @@ describe('RsInput', () => {
280
280
  })
281
281
  expect((wrapper.find('input').element as HTMLInputElement).value).toBe('256')
282
282
  })
283
+
284
+ it('places clear before custom suffix inside the affix', () => {
285
+ const wrapper = mount(RsInput, {
286
+ props: { modelValue: 'hub', clearable: true },
287
+ slots: { suffix: () => '选' },
288
+ })
289
+ const affix = wrapper.find('.rs-input-group__affix--suffix')
290
+ const children = [...affix.element.children] as HTMLElement[]
291
+ expect(children.map((el) => el.className)).toEqual([
292
+ 'rs-input-group__action',
293
+ 'rs-input-group__custom-suffix',
294
+ ])
295
+ })
296
+
297
+ it('renders addonAfterIcon as an owned addon button and emits addonAfterClick', async () => {
298
+ const wrapper = mount(RsInput, {
299
+ props: {
300
+ modelValue: 'hub',
301
+ clearable: true,
302
+ addonAfterIcon: 'ellipsis',
303
+ addonAfterIconLabel: '选择',
304
+ },
305
+ })
306
+ expect(wrapper.find('.rs-input-shell--combined').exists()).toBe(true)
307
+ expect(wrapper.find('.rs-input-addon--icon').exists()).toBe(true)
308
+ const button = wrapper.find('button.rs-input-addon__button')
309
+ expect(button.attributes('aria-label')).toBe('选择')
310
+ await button.trigger('click')
311
+ expect(wrapper.emitted('addonAfterClick')).toHaveLength(1)
312
+ })
313
+
314
+ it('prefers addonAfter content over addonAfterIcon', () => {
315
+ const wrapper = mount(RsInput, {
316
+ props: {
317
+ modelValue: '',
318
+ addonAfter: '.com',
319
+ addonAfterIcon: 'ellipsis',
320
+ },
321
+ })
322
+ expect(wrapper.find('.rs-input-addon--after').text()).toBe('.com')
323
+ expect(wrapper.find('button.rs-input-addon__button').exists()).toBe(false)
324
+ })
325
+
326
+ it('renders addonBefore and addonAfter as a combined shell', () => {
327
+ const wrapper = mount(RsInput, {
328
+ props: { modelValue: '', addonBefore: 'https://', addonAfter: '.com' },
329
+ })
330
+ expect(wrapper.find('.rs-input-shell--combined').exists()).toBe(true)
331
+ expect(wrapper.find('.rs-input-addon--before').text()).toBe('https://')
332
+ expect(wrapper.find('.rs-input-addon--after').text()).toBe('.com')
333
+ expect(wrapper.find('.rs-input-group--combined-before').exists()).toBe(true)
334
+ expect(wrapper.find('.rs-input-group--combined-after').exists()).toBe(true)
335
+ })
336
+
337
+ it('renders addonAfter slot as a combined shell', () => {
338
+ const wrapper = mount(RsInput, {
339
+ props: { modelValue: '' },
340
+ slots: { addonAfter: () => h('span', { class: 'slot-addon' }, '选') },
341
+ })
342
+ expect(wrapper.find('.rs-input-addon--after .slot-addon').text()).toBe('选')
343
+ expect(wrapper.find('.rs-input-group--combined-after').exists()).toBe(true)
344
+ })
345
+
346
+ it('does not emit pressEnter while IME is composing', async () => {
347
+ const wrapper = mount(RsInput, { props: { modelValue: '' } })
348
+ const input = wrapper.find('input')
349
+ await input.trigger('compositionstart')
350
+ await input.trigger('keydown', { key: 'Enter' })
351
+ expect(wrapper.emitted('pressEnter')).toBeFalsy()
352
+ await input.trigger('compositionend')
353
+ await input.trigger('keydown', { key: 'Enter' })
354
+ expect(wrapper.emitted('pressEnter')).toHaveLength(1)
355
+ })
356
+
357
+ it('does not emit validate while IME is composing', async () => {
358
+ const wrapper = mount(RsInput, {
359
+ props: {
360
+ modelValue: '',
361
+ required: true,
362
+ validateTrigger: 'input',
363
+ },
364
+ })
365
+ const input = wrapper.find('input')
366
+ await input.trigger('blur')
367
+ await input.trigger('compositionstart')
368
+ const before = wrapper.emitted('validate')?.length ?? 0
369
+ await input.trigger('input')
370
+ await flushPromises()
371
+ expect(wrapper.emitted('validate')?.length ?? 0).toBe(before)
372
+ await input.trigger('compositionend')
373
+ await flushPromises()
374
+ expect((wrapper.emitted('validate')?.length ?? 0)).toBeGreaterThan(before)
375
+ })
283
376
  })
@@ -108,6 +108,39 @@ describe('RsInputNumber', () => {
108
108
  expect(state.value).toBe(5)
109
109
  })
110
110
 
111
+ it('ignores wheel when changeOnWheel is off', async () => {
112
+ const { wrapper, state } = mountNumber({ modelValue: 5, step: 1 })
113
+ const input = wrapper.find('input')
114
+ await input.trigger('focus')
115
+ await input.trigger('wheel', { deltaY: -100 })
116
+ expect(state.value).toBe(5)
117
+ })
118
+
119
+ it('steps on wheel when changeOnWheel is on and focused', async () => {
120
+ const { wrapper, state } = mountNumber({
121
+ modelValue: 5,
122
+ step: 1,
123
+ changeOnWheel: true,
124
+ })
125
+ const input = wrapper.find('input')
126
+ await input.trigger('focus')
127
+ await input.trigger('wheel', { deltaY: -100 })
128
+ expect(state.value).toBe(6)
129
+ await input.trigger('wheel', { deltaY: 100 })
130
+ expect(state.value).toBe(5)
131
+ })
132
+
133
+ it('ignores wheel when changeOnWheel is on but not focused', async () => {
134
+ const { wrapper, state } = mountNumber({
135
+ modelValue: 5,
136
+ step: 1,
137
+ changeOnWheel: true,
138
+ })
139
+ const input = wrapper.find('input')
140
+ await input.trigger('wheel', { deltaY: -100 })
141
+ expect(state.value).toBe(5)
142
+ })
143
+
111
144
  it('clears to null on empty blur', async () => {
112
145
  const { wrapper, state } = mountNumber({ modelValue: 3 })
113
146
  const input = wrapper.find('input')