@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,10 @@
1
+ export { default as DataTable } from './DataTable.vue'
2
+ export type {
3
+ DataTableProps,
4
+ DataTableColumn,
5
+ SortState,
6
+ SortDirection,
7
+ ColumnAlign,
8
+ PaginationConfig,
9
+ FilterFn
10
+ } from './DataTable.vue'
@@ -0,0 +1,625 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { mount, config } from '@vue/test-utils'
3
+ import DatePicker from './DatePicker.vue'
4
+
5
+ config.global.stubs = {
6
+ teleport: true
7
+ }
8
+
9
+ const mockMatchMedia = (matches: Record<string, boolean> = {}) => {
10
+ return vi.fn().mockImplementation((query: string) => ({
11
+ matches: matches[query] ?? false,
12
+ media: query,
13
+ onchange: null,
14
+ addListener: vi.fn(),
15
+ removeListener: vi.fn(),
16
+ addEventListener: vi.fn(),
17
+ removeEventListener: vi.fn(),
18
+ dispatchEvent: vi.fn()
19
+ }))
20
+ }
21
+
22
+ describe('DatePicker', () => {
23
+ beforeEach(() => {
24
+ Object.defineProperty(window, 'matchMedia', {
25
+ writable: true,
26
+ value: mockMatchMedia({
27
+ '(max-width: 640px)': false,
28
+ '(any-hover: hover)': false
29
+ })
30
+ })
31
+ })
32
+
33
+ afterEach(() => {
34
+ vi.restoreAllMocks()
35
+ })
36
+
37
+ describe('Rendering', () => {
38
+ it('renders trigger element', () => {
39
+ const wrapper = mount(DatePicker)
40
+ expect(wrapper.find('.ui-datepicker-trigger').exists()).toBe(true)
41
+ })
42
+
43
+ it('renders with label', () => {
44
+ const wrapper = mount(DatePicker, {
45
+ props: { label: 'Birth Date' }
46
+ })
47
+ expect(wrapper.find('.ui-datepicker-field__label').text()).toBe('Birth Date')
48
+ })
49
+
50
+ it('renders with placeholder', () => {
51
+ const wrapper = mount(DatePicker, {
52
+ props: { placeholder: 'Pick a date' }
53
+ })
54
+ expect(wrapper.find('.ui-datepicker-trigger__value').text()).toBe('Pick a date')
55
+ })
56
+
57
+ it('renders with hint text', () => {
58
+ const wrapper = mount(DatePicker, {
59
+ props: { hint: 'Choose your preferred date' }
60
+ })
61
+ expect(wrapper.find('.ui-datepicker-field__message--hint').text()).toBe('Choose your preferred date')
62
+ })
63
+
64
+ it('renders with error message', () => {
65
+ const wrapper = mount(DatePicker, {
66
+ props: { error: 'Date is required' }
67
+ })
68
+ expect(wrapper.find('.ui-datepicker-field__message--error').text()).toBe('Date is required')
69
+ })
70
+
71
+ it('error takes precedence over hint', () => {
72
+ const wrapper = mount(DatePicker, {
73
+ props: { hint: 'Hint text', error: 'Error text' }
74
+ })
75
+ expect(wrapper.find('.ui-datepicker-field__message--error').exists()).toBe(true)
76
+ expect(wrapper.find('.ui-datepicker-field__message--hint').exists()).toBe(false)
77
+ })
78
+
79
+ it('renders required indicator', () => {
80
+ const wrapper = mount(DatePicker, {
81
+ props: { label: 'Date', required: true }
82
+ })
83
+ expect(wrapper.find('.ui-datepicker-field__required').exists()).toBe(true)
84
+ })
85
+
86
+ it('renders calendar icon in trigger', () => {
87
+ const wrapper = mount(DatePicker)
88
+ expect(wrapper.find('.ui-datepicker-trigger__icon').exists()).toBe(true)
89
+ })
90
+ })
91
+
92
+ describe('v-model', () => {
93
+ it('displays formatted date when value is set', () => {
94
+ const wrapper = mount(DatePicker, {
95
+ props: { modelValue: '2024-06-15' }
96
+ })
97
+ expect(wrapper.find('.ui-datepicker-trigger--has-value').exists()).toBe(true)
98
+ })
99
+
100
+ it('emits update:modelValue when date is selected', async () => {
101
+ const wrapper = mount(DatePicker, {
102
+ attachTo: document.body
103
+ })
104
+
105
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
106
+ await wrapper.vm.$nextTick()
107
+
108
+ const dayButton = wrapper.find('.ui-datepicker__day:not(.ui-datepicker__day--other-month):not([disabled])')
109
+ await dayButton.trigger('click')
110
+
111
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy()
112
+ })
113
+
114
+ it('shows selected date in calendar', async () => {
115
+ const wrapper = mount(DatePicker, {
116
+ props: { modelValue: '2024-06-15' },
117
+ attachTo: document.body
118
+ })
119
+
120
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
121
+ await wrapper.vm.$nextTick()
122
+
123
+ expect(wrapper.find('.ui-datepicker__day--selected').exists()).toBe(true)
124
+ })
125
+ })
126
+
127
+ describe('Size variants', () => {
128
+ it('applies xs size class', () => {
129
+ const wrapper = mount(DatePicker, {
130
+ props: { size: 'xs' }
131
+ })
132
+ expect(wrapper.find('.ui-datepicker-trigger').classes()).toContain('ui-datepicker-trigger--xs')
133
+ })
134
+
135
+ it('applies sm size class', () => {
136
+ const wrapper = mount(DatePicker, {
137
+ props: { size: 'sm' }
138
+ })
139
+ expect(wrapper.find('.ui-datepicker-trigger').classes()).toContain('ui-datepicker-trigger--sm')
140
+ })
141
+
142
+ it('applies md size class', () => {
143
+ const wrapper = mount(DatePicker, {
144
+ props: { size: 'md' }
145
+ })
146
+ expect(wrapper.find('.ui-datepicker-trigger').classes()).toContain('ui-datepicker-trigger--md')
147
+ })
148
+
149
+ it('applies lg size class', () => {
150
+ const wrapper = mount(DatePicker, {
151
+ props: { size: 'lg' }
152
+ })
153
+ expect(wrapper.find('.ui-datepicker-trigger').classes()).toContain('ui-datepicker-trigger--lg')
154
+ })
155
+
156
+ it('applies xl size class', () => {
157
+ const wrapper = mount(DatePicker, {
158
+ props: { size: 'xl' }
159
+ })
160
+ expect(wrapper.find('.ui-datepicker-trigger').classes()).toContain('ui-datepicker-trigger--xl')
161
+ })
162
+ })
163
+
164
+ describe('States', () => {
165
+ it('applies disabled state', () => {
166
+ const wrapper = mount(DatePicker, {
167
+ props: { disabled: true }
168
+ })
169
+ expect(wrapper.find('.ui-datepicker-trigger').classes()).toContain('ui-datepicker-trigger--disabled')
170
+ })
171
+
172
+ it('does not open popover when disabled', async () => {
173
+ const wrapper = mount(DatePicker, {
174
+ props: { disabled: true }
175
+ })
176
+
177
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
178
+ await wrapper.vm.$nextTick()
179
+
180
+ expect(wrapper.find('.ui-datepicker__grid').exists()).toBe(false)
181
+ })
182
+
183
+ it('applies error state', () => {
184
+ const wrapper = mount(DatePicker, {
185
+ props: { error: 'Error' }
186
+ })
187
+ expect(wrapper.find('.ui-datepicker-trigger').classes()).toContain('ui-datepicker-trigger--error')
188
+ })
189
+
190
+ it('applies block state', () => {
191
+ const wrapper = mount(DatePicker, {
192
+ props: { block: true }
193
+ })
194
+ expect(wrapper.find('.ui-datepicker-field').classes()).toContain('ui-datepicker-field--block')
195
+ })
196
+ })
197
+
198
+ describe('Calendar navigation', () => {
199
+ it('shows month and year in header', async () => {
200
+ const wrapper = mount(DatePicker, {
201
+ props: { modelValue: '2024-06-15' },
202
+ attachTo: document.body
203
+ })
204
+
205
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
206
+ await wrapper.vm.$nextTick()
207
+
208
+ expect(wrapper.find('.ui-datepicker__title-btn').text()).toContain('2024')
209
+ })
210
+
211
+ it('renders weekday headers', async () => {
212
+ const wrapper = mount(DatePicker, {
213
+ attachTo: document.body
214
+ })
215
+
216
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
217
+ await wrapper.vm.$nextTick()
218
+
219
+ const weekdays = wrapper.findAll('.ui-datepicker__weekday')
220
+ expect(weekdays.length).toBe(7)
221
+ })
222
+
223
+ it('renders 42 day cells (6 rows × 7 days)', async () => {
224
+ const wrapper = mount(DatePicker, {
225
+ attachTo: document.body
226
+ })
227
+
228
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
229
+ await wrapper.vm.$nextTick()
230
+
231
+ const days = wrapper.findAll('.ui-datepicker__day')
232
+ expect(days.length).toBe(42)
233
+ })
234
+ })
235
+
236
+ describe('Date constraints', () => {
237
+ it('disables dates before min date', async () => {
238
+ const wrapper = mount(DatePicker, {
239
+ props: {
240
+ modelValue: '2024-06-15',
241
+ min: '2024-06-10'
242
+ },
243
+ attachTo: document.body
244
+ })
245
+
246
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
247
+ await wrapper.vm.$nextTick()
248
+
249
+ const disabledDays = wrapper.findAll('.ui-datepicker__day--disabled')
250
+ expect(disabledDays.length).toBeGreaterThan(0)
251
+ })
252
+
253
+ it('disables dates after max date', async () => {
254
+ const wrapper = mount(DatePicker, {
255
+ props: {
256
+ modelValue: '2024-06-15',
257
+ max: '2024-06-20'
258
+ },
259
+ attachTo: document.body
260
+ })
261
+
262
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
263
+ await wrapper.vm.$nextTick()
264
+
265
+ const disabledDays = wrapper.findAll('.ui-datepicker__day--disabled')
266
+ expect(disabledDays.length).toBeGreaterThan(0)
267
+ })
268
+ })
269
+
270
+ describe('Accessibility', () => {
271
+ it('auto-generates unique id', () => {
272
+ const wrapper = mount(DatePicker)
273
+ const valueWrapper = wrapper.find('.ui-datepicker-trigger__value-wrapper')
274
+ expect(valueWrapper.attributes('id')).toMatch(/^datepicker-/)
275
+ })
276
+
277
+ it('uses provided id', () => {
278
+ const wrapper = mount(DatePicker, {
279
+ props: { id: 'custom-id' }
280
+ })
281
+ const valueWrapper = wrapper.find('.ui-datepicker-trigger__value-wrapper')
282
+ expect(valueWrapper.attributes('id')).toBe('custom-id')
283
+ })
284
+
285
+ it('links label to trigger via for attribute', () => {
286
+ const wrapper = mount(DatePicker, {
287
+ props: { label: 'Date', id: 'date-input' }
288
+ })
289
+ expect(wrapper.find('label').attributes('for')).toBe('date-input')
290
+ })
291
+
292
+ it('sets aria-describedby to hint id', () => {
293
+ const wrapper = mount(DatePicker, {
294
+ props: { hint: 'Helpful hint', id: 'my-picker' }
295
+ })
296
+ const valueWrapper = wrapper.find('.ui-datepicker-trigger__value-wrapper')
297
+ expect(valueWrapper.attributes('aria-describedby')).toBe('my-picker-hint')
298
+ })
299
+
300
+ it('sets aria-describedby to error id when error exists', () => {
301
+ const wrapper = mount(DatePicker, {
302
+ props: { error: 'Error message', id: 'my-picker' }
303
+ })
304
+ const valueWrapper = wrapper.find('.ui-datepicker-trigger__value-wrapper')
305
+ expect(valueWrapper.attributes('aria-describedby')).toBe('my-picker-error')
306
+ })
307
+
308
+ it('sets aria-invalid when error exists', () => {
309
+ const wrapper = mount(DatePicker, {
310
+ props: { error: 'Error message' }
311
+ })
312
+ const valueWrapper = wrapper.find('.ui-datepicker-trigger__value-wrapper')
313
+ expect(valueWrapper.attributes('aria-invalid')).toBe('true')
314
+ })
315
+
316
+ it('error message has role="alert"', () => {
317
+ const wrapper = mount(DatePicker, {
318
+ props: { error: 'Error message' }
319
+ })
320
+ expect(wrapper.find('.ui-datepicker-field__message--error').attributes('role')).toBe('alert')
321
+ })
322
+
323
+ it('trigger has role="combobox"', () => {
324
+ const wrapper = mount(DatePicker)
325
+ const valueWrapper = wrapper.find('.ui-datepicker-trigger__value-wrapper')
326
+ expect(valueWrapper.attributes('role')).toBe('combobox')
327
+ })
328
+
329
+ it('trigger has aria-haspopup="grid"', () => {
330
+ const wrapper = mount(DatePicker)
331
+ const valueWrapper = wrapper.find('.ui-datepicker-trigger__value-wrapper')
332
+ expect(valueWrapper.attributes('aria-haspopup')).toBe('grid')
333
+ })
334
+ })
335
+
336
+ describe('HTML attributes', () => {
337
+ it('passes name attribute to hidden input', () => {
338
+ const wrapper = mount(DatePicker, {
339
+ props: { name: 'birthdate' }
340
+ })
341
+ expect(wrapper.find('input[type="hidden"]').attributes('name')).toBe('birthdate')
342
+ })
343
+
344
+ it('passes required attribute to hidden input', () => {
345
+ const wrapper = mount(DatePicker, {
346
+ props: { name: 'date', required: true }
347
+ })
348
+ expect(wrapper.find('input[type="hidden"]').attributes('required')).toBeDefined()
349
+ })
350
+ })
351
+
352
+ describe('Exposed methods', () => {
353
+ it('exposes open method', () => {
354
+ const wrapper = mount(DatePicker)
355
+ expect(typeof wrapper.vm.open).toBe('function')
356
+ })
357
+
358
+ it('exposes close method', () => {
359
+ const wrapper = mount(DatePicker)
360
+ expect(typeof wrapper.vm.close).toBe('function')
361
+ })
362
+
363
+ it('exposes toggle method', async () => {
364
+ const wrapper = mount(DatePicker)
365
+ expect(typeof wrapper.vm.toggle).toBe('function')
366
+ })
367
+ })
368
+
369
+ describe('Custom formatting', () => {
370
+ it('uses custom formatDisplay function', () => {
371
+ const wrapper = mount(DatePicker, {
372
+ props: {
373
+ modelValue: '2024-06-15',
374
+ formatDisplay: (d: Date) => `${d.getDate()}/${d.getMonth() + 1}/${d.getFullYear()}`
375
+ }
376
+ })
377
+ expect(wrapper.find('.ui-datepicker-trigger__value').text()).toBe('15/6/2024')
378
+ })
379
+ })
380
+
381
+ describe('Range mode', () => {
382
+ it('accepts mode="range" prop', () => {
383
+ const wrapper = mount(DatePicker, {
384
+ props: { mode: 'range' }
385
+ })
386
+ expect(wrapper.vm.$props.mode).toBe('range')
387
+ })
388
+
389
+ it('displays range value as "start – end"', () => {
390
+ const wrapper = mount(DatePicker, {
391
+ props: {
392
+ mode: 'range',
393
+ modelValue: ['2024-06-10', '2024-06-15']
394
+ }
395
+ })
396
+ expect(wrapper.find('.ui-datepicker-trigger--has-value').exists()).toBe(true)
397
+ })
398
+
399
+ it('emits tuple for range selection', async () => {
400
+ const wrapper = mount(DatePicker, {
401
+ props: { mode: 'range' },
402
+ attachTo: document.body
403
+ })
404
+
405
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
406
+ await wrapper.vm.$nextTick()
407
+
408
+ const days = wrapper.findAll('.ui-datepicker__day:not(.ui-datepicker__day--other-month):not([disabled])')
409
+ await days[0].trigger('click')
410
+ await days[5].trigger('click')
411
+
412
+ const emitted = wrapper.emitted('update:modelValue')
413
+ expect(emitted).toBeTruthy()
414
+ expect(Array.isArray(emitted?.[0]?.[0])).toBe(true)
415
+ })
416
+
417
+ it('shows "Select end date" hint during selection', async () => {
418
+ const wrapper = mount(DatePicker, {
419
+ props: { mode: 'range' },
420
+ attachTo: document.body
421
+ })
422
+
423
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
424
+ await wrapper.vm.$nextTick()
425
+
426
+ const day = wrapper.find('.ui-datepicker__day:not(.ui-datepicker__day--other-month):not([disabled])')
427
+ await day.trigger('click')
428
+
429
+ expect(wrapper.find('.ui-datepicker__hint').text()).toBe('Select end date')
430
+ })
431
+
432
+ it('applies range-start and range-end classes', async () => {
433
+ const wrapper = mount(DatePicker, {
434
+ props: {
435
+ mode: 'range',
436
+ modelValue: ['2024-06-10', '2024-06-15']
437
+ },
438
+ attachTo: document.body
439
+ })
440
+
441
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
442
+ await wrapper.vm.$nextTick()
443
+
444
+ expect(wrapper.find('.ui-datepicker__day--range-start').exists()).toBe(true)
445
+ expect(wrapper.find('.ui-datepicker__day--range-end').exists()).toBe(true)
446
+ })
447
+ })
448
+
449
+ describe('ARIA grid roles', () => {
450
+ it('grid has role="grid"', async () => {
451
+ const wrapper = mount(DatePicker, {
452
+ attachTo: document.body
453
+ })
454
+
455
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
456
+ await wrapper.vm.$nextTick()
457
+
458
+ expect(wrapper.find('.ui-datepicker__grid').attributes('role')).toBe('grid')
459
+ })
460
+
461
+ it('day cells have role="gridcell"', async () => {
462
+ const wrapper = mount(DatePicker, {
463
+ attachTo: document.body
464
+ })
465
+
466
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
467
+ await wrapper.vm.$nextTick()
468
+
469
+ const firstDay = wrapper.find('.ui-datepicker__day')
470
+ expect(firstDay.attributes('role')).toBe('gridcell')
471
+ })
472
+
473
+ it('today has aria-current="date"', async () => {
474
+ const wrapper = mount(DatePicker, {
475
+ attachTo: document.body
476
+ })
477
+
478
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
479
+ await wrapper.vm.$nextTick()
480
+
481
+ const todayCell = wrapper.find('.ui-datepicker__day--today')
482
+ if (todayCell.exists()) {
483
+ expect(todayCell.attributes('aria-current')).toBe('date')
484
+ }
485
+ })
486
+
487
+ it('weekday headers have role="columnheader"', async () => {
488
+ const wrapper = mount(DatePicker, {
489
+ attachTo: document.body
490
+ })
491
+
492
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
493
+ await wrapper.vm.$nextTick()
494
+
495
+ const weekday = wrapper.find('.ui-datepicker__weekday')
496
+ expect(weekday.attributes('role')).toBe('columnheader')
497
+ })
498
+ })
499
+
500
+ describe('Year/Month drilldown', () => {
501
+ it('switches to year view when clicking header', async () => {
502
+ const wrapper = mount(DatePicker, {
503
+ attachTo: document.body
504
+ })
505
+
506
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
507
+ await wrapper.vm.$nextTick()
508
+
509
+ await wrapper.find('.ui-datepicker__title-btn').trigger('click')
510
+ await wrapper.vm.$nextTick()
511
+
512
+ expect(wrapper.find('.ui-datepicker__year-grid').exists()).toBe(true)
513
+ })
514
+
515
+ it('shows 20 years in year grid', async () => {
516
+ const wrapper = mount(DatePicker, {
517
+ attachTo: document.body
518
+ })
519
+
520
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
521
+ await wrapper.vm.$nextTick()
522
+
523
+ await wrapper.find('.ui-datepicker__title-btn').trigger('click')
524
+ await wrapper.vm.$nextTick()
525
+
526
+ const years = wrapper.findAll('.ui-datepicker__year')
527
+ expect(years.length).toBe(20)
528
+ })
529
+
530
+ it('switches to month view when year is selected', async () => {
531
+ const wrapper = mount(DatePicker, {
532
+ attachTo: document.body
533
+ })
534
+
535
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
536
+ await wrapper.vm.$nextTick()
537
+
538
+ await wrapper.find('.ui-datepicker__title-btn').trigger('click')
539
+ await wrapper.vm.$nextTick()
540
+
541
+ await wrapper.find('.ui-datepicker__year').trigger('click')
542
+ await wrapper.vm.$nextTick()
543
+
544
+ expect(wrapper.find('.ui-datepicker__month-grid').exists()).toBe(true)
545
+ })
546
+
547
+ it('shows 12 months in month grid', async () => {
548
+ const wrapper = mount(DatePicker, {
549
+ attachTo: document.body
550
+ })
551
+
552
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
553
+ await wrapper.vm.$nextTick()
554
+
555
+ await wrapper.find('.ui-datepicker__title-btn').trigger('click')
556
+ await wrapper.vm.$nextTick()
557
+
558
+ await wrapper.find('.ui-datepicker__year').trigger('click')
559
+ await wrapper.vm.$nextTick()
560
+
561
+ const months = wrapper.findAll('.ui-datepicker__month')
562
+ expect(months.length).toBe(12)
563
+ })
564
+
565
+ it('returns to day view when month is selected', async () => {
566
+ const wrapper = mount(DatePicker, {
567
+ attachTo: document.body
568
+ })
569
+
570
+ await wrapper.find('.ui-datepicker-trigger').trigger('click')
571
+ await wrapper.vm.$nextTick()
572
+
573
+ await wrapper.find('.ui-datepicker__title-btn').trigger('click')
574
+ await wrapper.vm.$nextTick()
575
+
576
+ await wrapper.find('.ui-datepicker__year').trigger('click')
577
+ await wrapper.vm.$nextTick()
578
+
579
+ await wrapper.find('.ui-datepicker__month').trigger('click')
580
+ await wrapper.vm.$nextTick()
581
+
582
+ expect(wrapper.find('.ui-datepicker__grid').exists()).toBe(true)
583
+ })
584
+ })
585
+
586
+ describe('Hybrid input mode', () => {
587
+ it('renders editable input on desktop with pointer', async () => {
588
+ Object.defineProperty(window, 'matchMedia', {
589
+ writable: true,
590
+ value: mockMatchMedia({
591
+ '(max-width: 640px)': false,
592
+ '(any-hover: hover)': true
593
+ })
594
+ })
595
+
596
+ const wrapper = mount(DatePicker, {
597
+ attachTo: document.body
598
+ })
599
+
600
+ expect(wrapper.find('.ui-datepicker-trigger__input').exists()).toBe(true)
601
+ })
602
+
603
+ it('renders value wrapper on touch devices', () => {
604
+ const wrapper = mount(DatePicker)
605
+ expect(wrapper.find('.ui-datepicker-trigger__value-wrapper').exists()).toBe(true)
606
+ })
607
+
608
+ it('renders value wrapper for range mode even on desktop', async () => {
609
+ Object.defineProperty(window, 'matchMedia', {
610
+ writable: true,
611
+ value: mockMatchMedia({
612
+ '(max-width: 640px)': false,
613
+ '(any-hover: hover)': true
614
+ })
615
+ })
616
+
617
+ const wrapper = mount(DatePicker, {
618
+ props: { mode: 'range' },
619
+ attachTo: document.body
620
+ })
621
+
622
+ expect(wrapper.find('.ui-datepicker-trigger__value-wrapper').exists()).toBe(true)
623
+ })
624
+ })
625
+ })