@sabrenski/spire-ui 0.0.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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/index.d.ts +4981 -0
  4. package/dist/spire-ui.css +1 -0
  5. package/dist/spire-ui.es.js +18403 -0
  6. package/dist/spire-ui.umd.js +45 -0
  7. package/package.json +83 -0
  8. package/src/components/Accordion/Accordion.test.ts +218 -0
  9. package/src/components/Accordion/AccordionContent.vue +112 -0
  10. package/src/components/Accordion/AccordionItem.vue +87 -0
  11. package/src/components/Accordion/AccordionRoot.vue +111 -0
  12. package/src/components/Accordion/AccordionTrigger.vue +125 -0
  13. package/src/components/Accordion/index.ts +11 -0
  14. package/src/components/Accordion/keys.ts +23 -0
  15. package/src/components/Avatar/Avatar.test.ts +181 -0
  16. package/src/components/Avatar/Avatar.vue +150 -0
  17. package/src/components/Avatar/index.ts +2 -0
  18. package/src/components/Badge/Badge.test.ts +141 -0
  19. package/src/components/Badge/Badge.vue +133 -0
  20. package/src/components/Badge/index.ts +2 -0
  21. package/src/components/BadgeContainer/BadgeContainer.test.ts +150 -0
  22. package/src/components/BadgeContainer/BadgeContainer.vue +90 -0
  23. package/src/components/BadgeContainer/index.ts +2 -0
  24. package/src/components/Breadcrumb/Breadcrumb.test.ts +342 -0
  25. package/src/components/Breadcrumb/BreadcrumbEllipsis.vue +96 -0
  26. package/src/components/Breadcrumb/BreadcrumbItem.vue +16 -0
  27. package/src/components/Breadcrumb/BreadcrumbLink.vue +67 -0
  28. package/src/components/Breadcrumb/BreadcrumbList.vue +20 -0
  29. package/src/components/Breadcrumb/BreadcrumbPage.vue +25 -0
  30. package/src/components/Breadcrumb/BreadcrumbRoot.vue +41 -0
  31. package/src/components/Breadcrumb/BreadcrumbSeparator.vue +63 -0
  32. package/src/components/Breadcrumb/index.ts +13 -0
  33. package/src/components/Breadcrumb/keys.ts +7 -0
  34. package/src/components/Button/Button.test.ts +231 -0
  35. package/src/components/Button/Button.vue +349 -0
  36. package/src/components/Button/index.ts +2 -0
  37. package/src/components/Callout/Callout.test.ts +260 -0
  38. package/src/components/Callout/Callout.vue +341 -0
  39. package/src/components/Callout/index.ts +2 -0
  40. package/src/components/Card/Card.test.ts +565 -0
  41. package/src/components/Card/Card.vue +209 -0
  42. package/src/components/Card/CardContent.vue +57 -0
  43. package/src/components/Card/CardFooter.vue +72 -0
  44. package/src/components/Card/CardHeader.vue +111 -0
  45. package/src/components/Card/CardImage.vue +124 -0
  46. package/src/components/Card/index.ts +14 -0
  47. package/src/components/Chart/BarChart.vue +208 -0
  48. package/src/components/Chart/BaseChart.vue +444 -0
  49. package/src/components/Chart/Chart.test.ts +359 -0
  50. package/src/components/Chart/DonutChart.vue +283 -0
  51. package/src/components/Chart/LineChart.vue +211 -0
  52. package/src/components/Chart/index.ts +20 -0
  53. package/src/components/Chart/useChartTheme.ts +192 -0
  54. package/src/components/Checkbox/Checkbox.test.ts +209 -0
  55. package/src/components/Checkbox/Checkbox.vue +285 -0
  56. package/src/components/Checkbox/index.ts +2 -0
  57. package/src/components/ChoiceChip/ChoiceChip.test.ts +142 -0
  58. package/src/components/ChoiceChip/ChoiceChip.vue +218 -0
  59. package/src/components/ChoiceChip/index.ts +2 -0
  60. package/src/components/ChoiceChipGroup/ChoiceChipGroup.test.ts +151 -0
  61. package/src/components/ChoiceChipGroup/ChoiceChipGroup.vue +70 -0
  62. package/src/components/ChoiceChipGroup/index.ts +2 -0
  63. package/src/components/ColorPicker/ColorArea.vue +159 -0
  64. package/src/components/ColorPicker/ColorPicker.test.ts +250 -0
  65. package/src/components/ColorPicker/ColorPicker.vue +339 -0
  66. package/src/components/ColorPicker/ColorSlider.vue +191 -0
  67. package/src/components/ColorPicker/index.ts +7 -0
  68. package/src/components/Combobox/Combobox.test.ts +891 -0
  69. package/src/components/Combobox/Combobox.vue +934 -0
  70. package/src/components/Combobox/index.ts +2 -0
  71. package/src/components/DataTable/DataTable.test.ts +1221 -0
  72. package/src/components/DataTable/DataTable.vue +1415 -0
  73. package/src/components/DataTable/index.ts +10 -0
  74. package/src/components/DatePicker/DatePicker.test.ts +625 -0
  75. package/src/components/DatePicker/DatePicker.vue +1586 -0
  76. package/src/components/DatePicker/index.ts +2 -0
  77. package/src/components/Drawer/Drawer.test.ts +336 -0
  78. package/src/components/Drawer/Drawer.vue +466 -0
  79. package/src/components/Drawer/index.ts +2 -0
  80. package/src/components/Dropdown/Dropdown.test.ts +607 -0
  81. package/src/components/Dropdown/Dropdown.vue +807 -0
  82. package/src/components/Dropdown/DropdownItem.vue +227 -0
  83. package/src/components/Dropdown/DropdownSeparator.vue +14 -0
  84. package/src/components/Dropdown/DropdownSub.vue +104 -0
  85. package/src/components/Dropdown/DropdownSubContent.vue +187 -0
  86. package/src/components/Dropdown/DropdownSubTrigger.vue +151 -0
  87. package/src/components/Dropdown/index.ts +14 -0
  88. package/src/components/EmptyState/EmptyState.test.ts +180 -0
  89. package/src/components/EmptyState/EmptyState.vue +137 -0
  90. package/src/components/EmptyState/index.ts +2 -0
  91. package/src/components/FileUpload/FileUpload.test.ts +1151 -0
  92. package/src/components/FileUpload/FileUpload.vue +1042 -0
  93. package/src/components/FileUpload/index.ts +2 -0
  94. package/src/components/Heading/Heading.test.ts +107 -0
  95. package/src/components/Heading/Heading.vue +67 -0
  96. package/src/components/Heading/index.ts +2 -0
  97. package/src/components/Icon/Icon.test.ts +157 -0
  98. package/src/components/Icon/Icon.vue +86 -0
  99. package/src/components/Icon/index.ts +2 -0
  100. package/src/components/Input/Input.test.ts +273 -0
  101. package/src/components/Input/Input.vue +388 -0
  102. package/src/components/Input/index.ts +2 -0
  103. package/src/components/Layout/Container.vue +67 -0
  104. package/src/components/Layout/Grid.vue +159 -0
  105. package/src/components/Layout/GridItem.vue +154 -0
  106. package/src/components/Layout/Layout.test.ts +202 -0
  107. package/src/components/Layout/Stack.vue +128 -0
  108. package/src/components/Layout/index.ts +9 -0
  109. package/src/components/Layout/keys.ts +7 -0
  110. package/src/components/Modal/Modal.test.ts +311 -0
  111. package/src/components/Modal/Modal.vue +336 -0
  112. package/src/components/Modal/index.ts +2 -0
  113. package/src/components/Pagination/Pagination.test.ts +303 -0
  114. package/src/components/Pagination/Pagination.vue +212 -0
  115. package/src/components/Pagination/index.ts +3 -0
  116. package/src/components/Pagination/utils.ts +86 -0
  117. package/src/components/Popover/Popover.test.ts +285 -0
  118. package/src/components/Popover/Popover.vue +441 -0
  119. package/src/components/Popover/index.ts +2 -0
  120. package/src/components/Progress/Progress.test.ts +361 -0
  121. package/src/components/Progress/Progress.vue +363 -0
  122. package/src/components/Progress/index.ts +7 -0
  123. package/src/components/Radio/Radio.test.ts +216 -0
  124. package/src/components/Radio/Radio.vue +214 -0
  125. package/src/components/Radio/index.ts +2 -0
  126. package/src/components/Rating/Rating.test.ts +319 -0
  127. package/src/components/Rating/Rating.vue +247 -0
  128. package/src/components/Rating/index.ts +2 -0
  129. package/src/components/SegmentedControl/SegmentedControl.test.ts +292 -0
  130. package/src/components/SegmentedControl/SegmentedControl.vue +288 -0
  131. package/src/components/SegmentedControl/index.ts +2 -0
  132. package/src/components/Select/Select.test.ts +589 -0
  133. package/src/components/Select/Select.vue +666 -0
  134. package/src/components/Select/index.ts +2 -0
  135. package/src/components/Sidebar/Sidebar.test.ts +301 -0
  136. package/src/components/Sidebar/SidebarGroup.vue +103 -0
  137. package/src/components/Sidebar/SidebarItem.vue +196 -0
  138. package/src/components/Sidebar/SidebarLayout.vue +42 -0
  139. package/src/components/Sidebar/SidebarRoot.vue +122 -0
  140. package/src/components/Sidebar/index.ts +11 -0
  141. package/src/components/Sidebar/keys.ts +14 -0
  142. package/src/components/Skeleton/Skeleton.test.ts +130 -0
  143. package/src/components/Skeleton/Skeleton.vue +104 -0
  144. package/src/components/Skeleton/index.ts +2 -0
  145. package/src/components/Slider/Slider.test.ts +416 -0
  146. package/src/components/Slider/Slider.vue +435 -0
  147. package/src/components/Slider/index.ts +2 -0
  148. package/src/components/Slider/utils.ts +91 -0
  149. package/src/components/Spinner/Spinner.test.ts +79 -0
  150. package/src/components/Spinner/Spinner.vue +159 -0
  151. package/src/components/Spinner/index.ts +2 -0
  152. package/src/components/SpireProvider/SpireProvider.vue +71 -0
  153. package/src/components/SpireProvider/index.ts +11 -0
  154. package/src/components/Stepper/Stepper.test.ts +221 -0
  155. package/src/components/Stepper/StepperContent.vue +51 -0
  156. package/src/components/Stepper/StepperItem.vue +89 -0
  157. package/src/components/Stepper/StepperRoot.vue +101 -0
  158. package/src/components/Stepper/StepperSeparator.vue +52 -0
  159. package/src/components/Stepper/StepperTrigger.vue +144 -0
  160. package/src/components/Stepper/index.ts +11 -0
  161. package/src/components/Stepper/keys.ts +27 -0
  162. package/src/components/Switch/Switch.test.ts +214 -0
  163. package/src/components/Switch/Switch.vue +235 -0
  164. package/src/components/Switch/index.ts +2 -0
  165. package/src/components/Tabs/Tabs.test.ts +363 -0
  166. package/src/components/Tabs/Tabs.vue +318 -0
  167. package/src/components/Tabs/index.ts +2 -0
  168. package/src/components/Text/Text.test.ts +154 -0
  169. package/src/components/Text/Text.vue +100 -0
  170. package/src/components/Text/index.ts +2 -0
  171. package/src/components/Textarea/Textarea.test.ts +432 -0
  172. package/src/components/Textarea/Textarea.vue +411 -0
  173. package/src/components/Textarea/index.ts +2 -0
  174. package/src/components/TimePicker/TimePicker.test.ts +352 -0
  175. package/src/components/TimePicker/TimePicker.vue +569 -0
  176. package/src/components/TimePicker/index.ts +2 -0
  177. package/src/components/Timeline/Timeline.test.ts +193 -0
  178. package/src/components/Timeline/Timeline.vue +111 -0
  179. package/src/components/Timeline/TimelineItem.vue +167 -0
  180. package/src/components/Timeline/index.ts +13 -0
  181. package/src/components/Timeline/keys.ts +21 -0
  182. package/src/components/Toast/ToastItem.test.ts +289 -0
  183. package/src/components/Toast/ToastItem.vue +370 -0
  184. package/src/components/Toast/ToastProvider.test.ts +158 -0
  185. package/src/components/Toast/ToastProvider.vue +181 -0
  186. package/src/components/Toast/index.ts +83 -0
  187. package/src/components/Toast/toastState.test.ts +165 -0
  188. package/src/components/Toast/toastState.ts +161 -0
  189. package/src/components/ToggleButton/ToggleButton.test.ts +166 -0
  190. package/src/components/ToggleButton/ToggleButton.vue +197 -0
  191. package/src/components/ToggleButton/index.ts +2 -0
  192. package/src/components/ToggleGroup/ToggleGroup.test.ts +181 -0
  193. package/src/components/ToggleGroup/ToggleGroup.vue +130 -0
  194. package/src/components/ToggleGroup/index.ts +2 -0
  195. package/src/components/Tooltip/Tooltip.test.ts +238 -0
  196. package/src/components/Tooltip/Tooltip.vue +217 -0
  197. package/src/components/Tooltip/index.ts +2 -0
  198. package/src/components/TreeView/TreeView.test.ts +357 -0
  199. package/src/components/TreeView/TreeView.vue +251 -0
  200. package/src/components/TreeView/TreeViewItem.vue +288 -0
  201. package/src/components/TreeView/index.ts +11 -0
  202. package/src/components/TreeView/keys.ts +35 -0
  203. package/src/composables/index.ts +12 -0
  204. package/src/composables/useClickOutside.ts +36 -0
  205. package/src/composables/useClipboard.ts +35 -0
  206. package/src/composables/useEventListener.ts +48 -0
  207. package/src/composables/useFocusTrap.ts +58 -0
  208. package/src/composables/useHoverReveal.ts +98 -0
  209. package/src/composables/useId.ts +10 -0
  210. package/src/composables/useMagnetic.ts +171 -0
  211. package/src/composables/useRelativePosition.ts +127 -0
  212. package/src/composables/useRipple.ts +146 -0
  213. package/src/composables/useScrollLock.ts +25 -0
  214. package/src/composables/useSpireConfig.ts +27 -0
  215. package/src/composables/useStagger.ts +224 -0
  216. package/src/config/icons.test.ts +115 -0
  217. package/src/config/icons.ts +170 -0
  218. package/src/index.ts +361 -0
  219. package/src/styles/depth.css +129 -0
  220. package/src/styles/effects.css +169 -0
  221. package/src/styles/fallback.css +152 -0
  222. package/src/styles/main.css +25 -0
  223. package/src/styles/mood.css +211 -0
  224. package/src/styles/motion.css +159 -0
  225. package/src/styles/reset.css +97 -0
  226. package/src/styles/theme.css +708 -0
  227. package/src/styles/tokens.css +183 -0
  228. package/src/utils/.gitkeep +0 -0
  229. package/src/utils/color.ts +277 -0
  230. package/src/utils/date.test.ts +522 -0
  231. package/src/utils/date.ts +380 -0
  232. package/src/utils/index.ts +23 -0
  233. package/src/utils/object.test.ts +80 -0
  234. package/src/utils/object.ts +25 -0
  235. package/src/utils/string.test.ts +64 -0
  236. package/src/utils/string.ts +32 -0
  237. package/src/utils/time.ts +156 -0
@@ -0,0 +1,416 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import Slider from './Slider.vue'
4
+ import { valueToPercent, percentToValue, snapToStep, clamp, getClosestThumb } from './utils'
5
+
6
+ describe('Slider utils', () => {
7
+ describe('valueToPercent', () => {
8
+ it('converts value to percentage', () => {
9
+ expect(valueToPercent(50, 0, 100)).toBe(50)
10
+ expect(valueToPercent(0, 0, 100)).toBe(0)
11
+ expect(valueToPercent(100, 0, 100)).toBe(100)
12
+ })
13
+
14
+ it('handles custom min/max range', () => {
15
+ expect(valueToPercent(50, 0, 200)).toBe(25)
16
+ expect(valueToPercent(150, 100, 200)).toBe(50)
17
+ })
18
+
19
+ it('returns 0 when min equals max', () => {
20
+ expect(valueToPercent(50, 50, 50)).toBe(0)
21
+ })
22
+ })
23
+
24
+ describe('percentToValue', () => {
25
+ it('converts percentage to value', () => {
26
+ expect(percentToValue(50, 0, 100)).toBe(50)
27
+ expect(percentToValue(0, 0, 100)).toBe(0)
28
+ expect(percentToValue(100, 0, 100)).toBe(100)
29
+ })
30
+
31
+ it('handles custom min/max range', () => {
32
+ expect(percentToValue(25, 0, 200)).toBe(50)
33
+ expect(percentToValue(50, 100, 200)).toBe(150)
34
+ })
35
+ })
36
+
37
+ describe('snapToStep', () => {
38
+ it('snaps to nearest step', () => {
39
+ expect(snapToStep(23, 5, 0)).toBe(25)
40
+ expect(snapToStep(22, 5, 0)).toBe(20)
41
+ expect(snapToStep(2.3, 0.5, 0)).toBe(2.5)
42
+ })
43
+
44
+ it('respects min offset', () => {
45
+ expect(snapToStep(13, 5, 3)).toBe(13)
46
+ expect(snapToStep(14, 5, 3)).toBe(13)
47
+ expect(snapToStep(16, 5, 3)).toBe(18)
48
+ })
49
+ })
50
+
51
+ describe('clamp', () => {
52
+ it('clamps value between min and max', () => {
53
+ expect(clamp(50, 0, 100)).toBe(50)
54
+ expect(clamp(-10, 0, 100)).toBe(0)
55
+ expect(clamp(150, 0, 100)).toBe(100)
56
+ })
57
+ })
58
+
59
+ describe('getClosestThumb', () => {
60
+ it('returns closest thumb', () => {
61
+ expect(getClosestThumb(30, 20, 80)).toBe('min')
62
+ expect(getClosestThumb(70, 20, 80)).toBe('max')
63
+ })
64
+
65
+ it('returns min when equidistant', () => {
66
+ expect(getClosestThumb(50, 20, 80)).toBe('min')
67
+ })
68
+ })
69
+ })
70
+
71
+ describe('Slider', () => {
72
+ describe('Rendering', () => {
73
+ it('renders with default props', () => {
74
+ const wrapper = mount(Slider, {
75
+ props: { modelValue: 50 }
76
+ })
77
+ expect(wrapper.find('.ui-slider').exists()).toBe(true)
78
+ expect(wrapper.find('.ui-slider__track').exists()).toBe(true)
79
+ expect(wrapper.find('.ui-slider__thumb').exists()).toBe(true)
80
+ })
81
+
82
+ it('renders range mode with two thumbs', () => {
83
+ const wrapper = mount(Slider, {
84
+ props: { modelValue: [20, 80] }
85
+ })
86
+ const thumbs = wrapper.findAll('.ui-slider__thumb')
87
+ expect(thumbs).toHaveLength(2)
88
+ })
89
+
90
+ it('applies disabled class', () => {
91
+ const wrapper = mount(Slider, {
92
+ props: { modelValue: 50, disabled: true }
93
+ })
94
+ expect(wrapper.find('.ui-slider--disabled').exists()).toBe(true)
95
+ })
96
+
97
+ it('applies size classes', () => {
98
+ const sm = mount(Slider, { props: { modelValue: 50, size: 'sm' } })
99
+ const lg = mount(Slider, { props: { modelValue: 50, size: 'lg' } })
100
+
101
+ expect(sm.find('.ui-slider--sm').exists()).toBe(true)
102
+ expect(lg.find('.ui-slider--lg').exists()).toBe(true)
103
+ })
104
+
105
+ it('applies range class in range mode', () => {
106
+ const wrapper = mount(Slider, {
107
+ props: { modelValue: [20, 80] }
108
+ })
109
+ expect(wrapper.find('.ui-slider--range').exists()).toBe(true)
110
+ })
111
+ })
112
+
113
+ describe('Single value mode', () => {
114
+ it('positions thumb correctly', () => {
115
+ const wrapper = mount(Slider, {
116
+ props: { modelValue: 50, min: 0, max: 100 }
117
+ })
118
+ const thumb = wrapper.find('.ui-slider__thumb')
119
+ expect(thumb.attributes('style')).toContain('left: 50%')
120
+ })
121
+
122
+ it('positions range fill correctly', () => {
123
+ const wrapper = mount(Slider, {
124
+ props: { modelValue: 50, min: 0, max: 100 }
125
+ })
126
+ const range = wrapper.find('.ui-slider__range')
127
+ expect(range.attributes('style')).toContain('left: 0%')
128
+ expect(range.attributes('style')).toContain('right: 50%')
129
+ })
130
+ })
131
+
132
+ describe('Range mode', () => {
133
+ it('positions both thumbs correctly', () => {
134
+ const wrapper = mount(Slider, {
135
+ props: { modelValue: [20, 80], min: 0, max: 100 }
136
+ })
137
+ const thumbs = wrapper.findAll('.ui-slider__thumb')
138
+ expect(thumbs[0].attributes('style')).toContain('left: 20%')
139
+ expect(thumbs[1].attributes('style')).toContain('left: 80%')
140
+ })
141
+
142
+ it('positions range fill correctly', () => {
143
+ const wrapper = mount(Slider, {
144
+ props: { modelValue: [20, 80], min: 0, max: 100 }
145
+ })
146
+ const range = wrapper.find('.ui-slider__range')
147
+ expect(range.attributes('style')).toContain('left: 20%')
148
+ expect(range.attributes('style')).toContain('right: 20%')
149
+ })
150
+ })
151
+
152
+ describe('Keyboard navigation', () => {
153
+ it('increases value with ArrowRight', async () => {
154
+ const wrapper = mount(Slider, {
155
+ props: { modelValue: 50, step: 1 }
156
+ })
157
+ const thumb = wrapper.find('.ui-slider__thumb')
158
+ await thumb.trigger('keydown', { key: 'ArrowRight' })
159
+
160
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy()
161
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual([51])
162
+ })
163
+
164
+ it('decreases value with ArrowLeft', async () => {
165
+ const wrapper = mount(Slider, {
166
+ props: { modelValue: 50, step: 1 }
167
+ })
168
+ const thumb = wrapper.find('.ui-slider__thumb')
169
+ await thumb.trigger('keydown', { key: 'ArrowLeft' })
170
+
171
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual([49])
172
+ })
173
+
174
+ it('goes to min with Home key', async () => {
175
+ const wrapper = mount(Slider, {
176
+ props: { modelValue: 50, min: 0 }
177
+ })
178
+ const thumb = wrapper.find('.ui-slider__thumb')
179
+ await thumb.trigger('keydown', { key: 'Home' })
180
+
181
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual([0])
182
+ })
183
+
184
+ it('goes to max with End key', async () => {
185
+ const wrapper = mount(Slider, {
186
+ props: { modelValue: 50, max: 100 }
187
+ })
188
+ const thumb = wrapper.find('.ui-slider__thumb')
189
+ await thumb.trigger('keydown', { key: 'End' })
190
+
191
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual([100])
192
+ })
193
+
194
+ it('respects step size', async () => {
195
+ const wrapper = mount(Slider, {
196
+ props: { modelValue: 50, step: 10 }
197
+ })
198
+ const thumb = wrapper.find('.ui-slider__thumb')
199
+ await thumb.trigger('keydown', { key: 'ArrowRight' })
200
+
201
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual([60])
202
+ })
203
+
204
+ it('emits change event after keyboard interaction', async () => {
205
+ const wrapper = mount(Slider, {
206
+ props: { modelValue: 50, step: 1 }
207
+ })
208
+ const thumb = wrapper.find('.ui-slider__thumb')
209
+ await thumb.trigger('keydown', { key: 'ArrowRight' })
210
+
211
+ expect(wrapper.emitted('change')).toBeTruthy()
212
+ })
213
+
214
+ it('does not respond when disabled', async () => {
215
+ const wrapper = mount(Slider, {
216
+ props: { modelValue: 50, disabled: true }
217
+ })
218
+ const thumb = wrapper.find('.ui-slider__thumb')
219
+ await thumb.trigger('keydown', { key: 'ArrowRight' })
220
+
221
+ expect(wrapper.emitted('update:modelValue')).toBeFalsy()
222
+ })
223
+ })
224
+
225
+ describe('Range keyboard navigation', () => {
226
+ it('moves min thumb independently', async () => {
227
+ const wrapper = mount(Slider, {
228
+ props: { modelValue: [20, 80] }
229
+ })
230
+ const minThumb = wrapper.findAll('.ui-slider__thumb')[0]
231
+ await minThumb.trigger('keydown', { key: 'ArrowRight' })
232
+
233
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual([[21, 80]])
234
+ })
235
+
236
+ it('moves max thumb independently', async () => {
237
+ const wrapper = mount(Slider, {
238
+ props: { modelValue: [20, 80] }
239
+ })
240
+ const maxThumb = wrapper.findAll('.ui-slider__thumb')[1]
241
+ await maxThumb.trigger('keydown', { key: 'ArrowLeft' })
242
+
243
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual([[20, 79]])
244
+ })
245
+
246
+ it('prevents min thumb from crossing max', async () => {
247
+ const wrapper = mount(Slider, {
248
+ props: { modelValue: [79, 80] }
249
+ })
250
+ const minThumb = wrapper.findAll('.ui-slider__thumb')[0]
251
+ await minThumb.trigger('keydown', { key: 'ArrowRight' })
252
+ await minThumb.trigger('keydown', { key: 'ArrowRight' })
253
+
254
+ const emits = wrapper.emitted('update:modelValue')!
255
+ expect(emits[emits.length - 1]).toEqual([[80, 80]])
256
+ })
257
+
258
+ it('prevents max thumb from crossing min', async () => {
259
+ const wrapper = mount(Slider, {
260
+ props: { modelValue: [20, 21] }
261
+ })
262
+ const maxThumb = wrapper.findAll('.ui-slider__thumb')[1]
263
+ await maxThumb.trigger('keydown', { key: 'ArrowLeft' })
264
+ await maxThumb.trigger('keydown', { key: 'ArrowLeft' })
265
+
266
+ const emits = wrapper.emitted('update:modelValue')!
267
+ expect(emits[emits.length - 1]).toEqual([[20, 20]])
268
+ })
269
+ })
270
+
271
+ describe('Accessibility', () => {
272
+ it('has slider role on thumbs', () => {
273
+ const wrapper = mount(Slider, {
274
+ props: { modelValue: 50 }
275
+ })
276
+ const thumb = wrapper.find('.ui-slider__thumb')
277
+ expect(thumb.attributes('role')).toBe('slider')
278
+ })
279
+
280
+ it('has correct ARIA attributes', () => {
281
+ const wrapper = mount(Slider, {
282
+ props: { modelValue: 50, min: 0, max: 100 }
283
+ })
284
+ const thumb = wrapper.find('.ui-slider__thumb')
285
+ expect(thumb.attributes('aria-valuemin')).toBe('0')
286
+ expect(thumb.attributes('aria-valuemax')).toBe('100')
287
+ expect(thumb.attributes('aria-valuenow')).toBe('50')
288
+ })
289
+
290
+ it('has aria-disabled when disabled', () => {
291
+ const wrapper = mount(Slider, {
292
+ props: { modelValue: 50, disabled: true }
293
+ })
294
+ const thumb = wrapper.find('.ui-slider__thumb')
295
+ expect(thumb.attributes('aria-disabled')).toBe('true')
296
+ })
297
+
298
+ it('thumbs are focusable', () => {
299
+ const wrapper = mount(Slider, {
300
+ props: { modelValue: 50 }
301
+ })
302
+ const thumb = wrapper.find('.ui-slider__thumb')
303
+ expect(thumb.attributes('tabindex')).toBe('0')
304
+ })
305
+
306
+ it('range mode has appropriate ARIA on both thumbs', () => {
307
+ const wrapper = mount(Slider, {
308
+ props: { modelValue: [20, 80], min: 0, max: 100 }
309
+ })
310
+ const thumbs = wrapper.findAll('.ui-slider__thumb')
311
+
312
+ expect(thumbs[0].attributes('aria-valuemin')).toBe('0')
313
+ expect(thumbs[0].attributes('aria-valuemax')).toBe('80')
314
+ expect(thumbs[0].attributes('aria-valuenow')).toBe('20')
315
+ expect(thumbs[0].attributes('aria-label')).toBe('Minimum value')
316
+
317
+ expect(thumbs[1].attributes('aria-valuemin')).toBe('20')
318
+ expect(thumbs[1].attributes('aria-valuemax')).toBe('100')
319
+ expect(thumbs[1].attributes('aria-valuenow')).toBe('80')
320
+ expect(thumbs[1].attributes('aria-label')).toBe('Maximum value')
321
+ })
322
+ })
323
+
324
+ describe('Tooltip', () => {
325
+ it('does not show tooltip by default', () => {
326
+ const wrapper = mount(Slider, {
327
+ props: { modelValue: 50 }
328
+ })
329
+ expect(wrapper.find('.ui-slider__tooltip').exists()).toBe(false)
330
+ })
331
+
332
+ it('does not show tooltip when not dragging even with showTooltip', () => {
333
+ const wrapper = mount(Slider, {
334
+ props: { modelValue: 50, showTooltip: true }
335
+ })
336
+ expect(wrapper.find('.ui-slider__tooltip').exists()).toBe(false)
337
+ })
338
+ })
339
+
340
+ describe('Ticks', () => {
341
+ it('does not show ticks by default', () => {
342
+ const wrapper = mount(Slider, {
343
+ props: { modelValue: 50, step: 10 }
344
+ })
345
+ expect(wrapper.find('.ui-slider__ticks').exists()).toBe(false)
346
+ })
347
+
348
+ it('shows ticks when showTicks is true', () => {
349
+ const wrapper = mount(Slider, {
350
+ props: { modelValue: 50, step: 10, showTicks: true }
351
+ })
352
+ expect(wrapper.find('.ui-slider__ticks').exists()).toBe(true)
353
+ })
354
+
355
+ it('renders correct number of ticks (excluding first and last)', () => {
356
+ const wrapper = mount(Slider, {
357
+ props: { modelValue: 50, min: 0, max: 100, step: 20, showTicks: true }
358
+ })
359
+ const ticks = wrapper.findAll('.ui-slider__tick')
360
+ expect(ticks).toHaveLength(4)
361
+ })
362
+
363
+ it('positions ticks correctly (excluding edges)', () => {
364
+ const wrapper = mount(Slider, {
365
+ props: { modelValue: 50, min: 0, max: 100, step: 25, showTicks: true }
366
+ })
367
+ const ticks = wrapper.findAll('.ui-slider__tick')
368
+ expect(ticks).toHaveLength(3)
369
+ expect(ticks[0].attributes('style')).toContain('left: 25%')
370
+ expect(ticks[1].attributes('style')).toContain('left: 50%')
371
+ expect(ticks[2].attributes('style')).toContain('left: 75%')
372
+ })
373
+
374
+ it('does not render ticks if too many steps', () => {
375
+ const wrapper = mount(Slider, {
376
+ props: { modelValue: 50, min: 0, max: 1000, step: 1, showTicks: true }
377
+ })
378
+ expect(wrapper.find('.ui-slider__tick').exists()).toBe(false)
379
+ })
380
+
381
+ it('does not render ticks if only two steps (would be empty)', () => {
382
+ const wrapper = mount(Slider, {
383
+ props: { modelValue: 50, min: 0, max: 100, step: 100, showTicks: true }
384
+ })
385
+ expect(wrapper.find('.ui-slider__tick').exists()).toBe(false)
386
+ })
387
+ })
388
+
389
+ describe('Edge cases', () => {
390
+ it('handles min equals max', () => {
391
+ const wrapper = mount(Slider, {
392
+ props: { modelValue: 50, min: 50, max: 50 }
393
+ })
394
+ expect(wrapper.find('.ui-slider').exists()).toBe(true)
395
+ })
396
+
397
+ it('handles negative values', () => {
398
+ const wrapper = mount(Slider, {
399
+ props: { modelValue: -25, min: -100, max: 0 }
400
+ })
401
+ const thumb = wrapper.find('.ui-slider__thumb')
402
+ expect(thumb.attributes('style')).toContain('left: 75%')
403
+ })
404
+
405
+ it('handles decimal step values', async () => {
406
+ const wrapper = mount(Slider, {
407
+ props: { modelValue: 0.5, min: 0, max: 1, step: 0.1 }
408
+ })
409
+ const thumb = wrapper.find('.ui-slider__thumb')
410
+ await thumb.trigger('keydown', { key: 'ArrowRight' })
411
+
412
+ const emitted = wrapper.emitted('update:modelValue')![0][0] as number
413
+ expect(emitted).toBeCloseTo(0.6, 5)
414
+ })
415
+ })
416
+ })