@sabrenski/spire-ui 0.0.5 → 0.0.7
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.
- package/dist/index.d.ts +170 -4
- package/dist/spire-ui.css +1 -1
- package/dist/spire-ui.es.js +7040 -6773
- package/dist/spire-ui.umd.js +10 -10
- package/package.json +83 -70
- package/src/components/Accordion/AccordionContent.vue +5 -2
- package/src/components/Accordion/AccordionItem.vue +4 -0
- package/src/components/Accordion/AccordionRoot.vue +4 -2
- package/src/components/Accordion/AccordionTrigger.vue +4 -1
- package/src/components/Avatar/Avatar.vue +4 -0
- package/src/components/Badge/Badge.vue +4 -0
- package/src/components/BadgeContainer/BadgeContainer.vue +4 -1
- package/src/components/Breadcrumb/BreadcrumbLink.vue +4 -1
- package/src/components/Breadcrumb/BreadcrumbRoot.vue +4 -1
- package/src/components/Button/Button.vue +5 -1
- package/src/components/Callout/Callout.vue +4 -0
- package/src/components/Card/Card.vue +5 -1
- package/src/components/Card/CardContent.vue +5 -1
- package/src/components/Card/CardFooter.vue +5 -1
- package/src/components/Card/CardHeader.vue +5 -1
- package/src/components/Card/CardImage.vue +4 -2
- package/src/components/Chart/BarChart.vue +4 -0
- package/src/components/Chart/BaseChart.vue +52 -47
- package/src/components/Chart/DonutChart.vue +4 -2
- package/src/components/Chart/LineChart.vue +4 -0
- package/src/components/Checkbox/Checkbox.test.ts +94 -0
- package/src/components/Checkbox/Checkbox.vue +170 -1
- package/src/components/ChoiceChip/ChoiceChip.vue +11 -5
- package/src/components/ChoiceChipGroup/ChoiceChipGroup.vue +4 -2
- package/src/components/ColorPicker/ColorArea.vue +4 -2
- package/src/components/ColorPicker/ColorPicker.vue +4 -2
- package/src/components/ColorPicker/ColorSlider.vue +5 -1
- package/src/components/Combobox/Combobox.vue +97 -91
- package/src/components/DataTable/DataTable.vue +5 -1
- package/src/components/DatePicker/DatePicker.vue +5 -1
- package/src/components/Drawer/Drawer.vue +13 -3
- package/src/components/Dropdown/Dropdown.vue +4 -2
- package/src/components/Dropdown/DropdownItem.vue +4 -0
- package/src/components/Dropdown/DropdownSubTrigger.vue +4 -2
- package/src/components/EmptyState/EmptyState.vue +5 -1
- package/src/components/FileUpload/FileUpload.vue +12 -6
- package/src/components/Heading/Heading.vue +4 -0
- package/src/components/Icon/Icon.vue +5 -2
- package/src/components/Input/Input.vue +5 -1
- package/src/components/Layout/Container.vue +4 -0
- package/src/components/Layout/Grid.vue +4 -1
- package/src/components/Layout/GridItem.vue +4 -1
- package/src/components/Layout/Stack.vue +4 -0
- package/src/components/Modal/Modal.test.ts +68 -13
- package/src/components/Modal/Modal.vue +94 -91
- package/src/components/Pagination/Pagination.vue +5 -1
- package/src/components/Popover/Popover.vue +4 -1
- package/src/components/Progress/Progress.vue +5 -0
- package/src/components/Radio/Radio.test.ts +88 -0
- package/src/components/Radio/Radio.vue +169 -1
- package/src/components/Rating/Rating.vue +5 -1
- package/src/components/SegmentedControl/SegmentedControl.vue +5 -1
- package/src/components/Select/Select.vue +61 -55
- package/src/components/Sidebar/SidebarGroup.vue +4 -0
- package/src/components/Sidebar/SidebarItem.vue +4 -0
- package/src/components/Sidebar/SidebarLayout.vue +5 -2
- package/src/components/Sidebar/SidebarRoot.vue +4 -2
- package/src/components/Skeleton/Skeleton.vue +5 -1
- package/src/components/Slider/Slider.vue +5 -1
- package/src/components/Spinner/Spinner.vue +4 -1
- package/src/components/SpireProvider/SpireProvider.vue +4 -1
- package/src/components/Stepper/StepperItem.vue +4 -0
- package/src/components/Stepper/StepperRoot.vue +4 -2
- package/src/components/Stepper/StepperTrigger.vue +6 -2
- package/src/components/Switch/Switch.vue +5 -1
- package/src/components/Tabs/Tabs.vue +4 -1
- package/src/components/Text/Text.vue +4 -0
- package/src/components/Textarea/Textarea.vue +13 -7
- package/src/components/TimePicker/TimePicker.vue +5 -1
- package/src/components/Timeline/Timeline.vue +4 -0
- package/src/components/Timeline/TimelineItem.vue +4 -0
- package/src/components/Toast/ToastItem.vue +5 -1
- package/src/components/Toast/ToastProvider.vue +5 -3
- package/src/components/ToggleButton/ToggleButton.vue +5 -1
- package/src/components/ToggleGroup/ToggleGroup.vue +5 -1
- package/src/components/Tooltip/Tooltip.vue +9 -1
- package/src/components/TreeView/TreeView.vue +4 -1
- package/src/components/TreeView/TreeViewItem.vue +4 -0
- package/src/index.ts +3 -0
- package/src/styles/main.css +21 -21
- package/src/types/common.ts +4 -0
|
@@ -2,6 +2,14 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
|
2
2
|
import { mount } from '@vue/test-utils'
|
|
3
3
|
import Modal from './Modal.vue'
|
|
4
4
|
|
|
5
|
+
const globalConfig = {
|
|
6
|
+
global: {
|
|
7
|
+
stubs: {
|
|
8
|
+
teleport: true
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
// Track open state per dialog element using WeakMap (module-level to persist across mocks)
|
|
6
14
|
const openStates = new WeakMap<HTMLDialogElement, boolean>()
|
|
7
15
|
|
|
@@ -40,18 +48,33 @@ afterEach(() => {
|
|
|
40
48
|
|
|
41
49
|
describe('Modal', () => {
|
|
42
50
|
describe('Rendering', () => {
|
|
43
|
-
it('renders as a dialog element', () => {
|
|
44
|
-
const wrapper = mount(Modal
|
|
51
|
+
it('renders as a dialog element when open', () => {
|
|
52
|
+
const wrapper = mount(Modal, {
|
|
53
|
+
...globalConfig,
|
|
54
|
+
props: { modelValue: true }
|
|
55
|
+
})
|
|
45
56
|
expect(wrapper.find('dialog').exists()).toBe(true)
|
|
46
57
|
})
|
|
47
58
|
|
|
59
|
+
it('does not render dialog when closed', () => {
|
|
60
|
+
const wrapper = mount(Modal, {
|
|
61
|
+
...globalConfig,
|
|
62
|
+
props: { modelValue: false }
|
|
63
|
+
})
|
|
64
|
+
expect(wrapper.find('dialog').exists()).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
48
67
|
it('renders with ui-modal class', () => {
|
|
49
|
-
const wrapper = mount(Modal
|
|
68
|
+
const wrapper = mount(Modal, {
|
|
69
|
+
...globalConfig,
|
|
70
|
+
props: { modelValue: true }
|
|
71
|
+
})
|
|
50
72
|
expect(wrapper.find('.ui-modal').exists()).toBe(true)
|
|
51
73
|
})
|
|
52
74
|
|
|
53
75
|
it('renders slot content in body', () => {
|
|
54
76
|
const wrapper = mount(Modal, {
|
|
77
|
+
...globalConfig,
|
|
55
78
|
props: { modelValue: true },
|
|
56
79
|
slots: {
|
|
57
80
|
default: '<p>Modal content</p>'
|
|
@@ -62,6 +85,7 @@ describe('Modal', () => {
|
|
|
62
85
|
|
|
63
86
|
it('renders title when provided', () => {
|
|
64
87
|
const wrapper = mount(Modal, {
|
|
88
|
+
...globalConfig,
|
|
65
89
|
props: { modelValue: true, title: 'My Modal' }
|
|
66
90
|
})
|
|
67
91
|
expect(wrapper.find('.ui-modal__title').text()).toBe('My Modal')
|
|
@@ -69,6 +93,7 @@ describe('Modal', () => {
|
|
|
69
93
|
|
|
70
94
|
it('does not render header when no title or header slot', () => {
|
|
71
95
|
const wrapper = mount(Modal, {
|
|
96
|
+
...globalConfig,
|
|
72
97
|
props: { modelValue: true }
|
|
73
98
|
})
|
|
74
99
|
expect(wrapper.find('.ui-modal__header').exists()).toBe(false)
|
|
@@ -76,6 +101,7 @@ describe('Modal', () => {
|
|
|
76
101
|
|
|
77
102
|
it('renders header slot instead of title', () => {
|
|
78
103
|
const wrapper = mount(Modal, {
|
|
104
|
+
...globalConfig,
|
|
79
105
|
props: { modelValue: true },
|
|
80
106
|
slots: {
|
|
81
107
|
header: '<span class="custom-header">Custom Header</span>'
|
|
@@ -86,6 +112,7 @@ describe('Modal', () => {
|
|
|
86
112
|
|
|
87
113
|
it('renders footer slot when provided', () => {
|
|
88
114
|
const wrapper = mount(Modal, {
|
|
115
|
+
...globalConfig,
|
|
89
116
|
props: { modelValue: true },
|
|
90
117
|
slots: {
|
|
91
118
|
footer: '<button>Save</button>'
|
|
@@ -97,6 +124,7 @@ describe('Modal', () => {
|
|
|
97
124
|
|
|
98
125
|
it('does not render footer when no slot provided', () => {
|
|
99
126
|
const wrapper = mount(Modal, {
|
|
127
|
+
...globalConfig,
|
|
100
128
|
props: { modelValue: true }
|
|
101
129
|
})
|
|
102
130
|
expect(wrapper.find('.ui-modal__footer').exists()).toBe(false)
|
|
@@ -106,33 +134,40 @@ describe('Modal', () => {
|
|
|
106
134
|
describe('Sizes', () => {
|
|
107
135
|
it('applies sm size class', () => {
|
|
108
136
|
const wrapper = mount(Modal, {
|
|
109
|
-
|
|
137
|
+
...globalConfig,
|
|
138
|
+
props: { modelValue: true, size: 'sm' }
|
|
110
139
|
})
|
|
111
140
|
expect(wrapper.find('.ui-modal').classes()).toContain('ui-modal--sm')
|
|
112
141
|
})
|
|
113
142
|
|
|
114
143
|
it('applies md size class by default', () => {
|
|
115
|
-
const wrapper = mount(Modal
|
|
144
|
+
const wrapper = mount(Modal, {
|
|
145
|
+
...globalConfig,
|
|
146
|
+
props: { modelValue: true }
|
|
147
|
+
})
|
|
116
148
|
expect(wrapper.find('.ui-modal').classes()).toContain('ui-modal--md')
|
|
117
149
|
})
|
|
118
150
|
|
|
119
151
|
it('applies lg size class', () => {
|
|
120
152
|
const wrapper = mount(Modal, {
|
|
121
|
-
|
|
153
|
+
...globalConfig,
|
|
154
|
+
props: { modelValue: true, size: 'lg' }
|
|
122
155
|
})
|
|
123
156
|
expect(wrapper.find('.ui-modal').classes()).toContain('ui-modal--lg')
|
|
124
157
|
})
|
|
125
158
|
|
|
126
159
|
it('applies xl size class', () => {
|
|
127
160
|
const wrapper = mount(Modal, {
|
|
128
|
-
|
|
161
|
+
...globalConfig,
|
|
162
|
+
props: { modelValue: true, size: 'xl' }
|
|
129
163
|
})
|
|
130
164
|
expect(wrapper.find('.ui-modal').classes()).toContain('ui-modal--xl')
|
|
131
165
|
})
|
|
132
166
|
|
|
133
167
|
it('applies full size class', () => {
|
|
134
168
|
const wrapper = mount(Modal, {
|
|
135
|
-
|
|
169
|
+
...globalConfig,
|
|
170
|
+
props: { modelValue: true, size: 'full' }
|
|
136
171
|
})
|
|
137
172
|
expect(wrapper.find('.ui-modal').classes()).toContain('ui-modal--full')
|
|
138
173
|
})
|
|
@@ -141,26 +176,30 @@ describe('Modal', () => {
|
|
|
141
176
|
describe('Open/Close', () => {
|
|
142
177
|
it('calls showModal when modelValue becomes true', async () => {
|
|
143
178
|
const wrapper = mount(Modal, {
|
|
179
|
+
...globalConfig,
|
|
144
180
|
props: { modelValue: false }
|
|
145
181
|
})
|
|
146
182
|
|
|
147
183
|
await wrapper.setProps({ modelValue: true })
|
|
184
|
+
await wrapper.vm.$nextTick()
|
|
148
185
|
|
|
149
186
|
expect(showModalMock).toHaveBeenCalled()
|
|
150
187
|
})
|
|
151
188
|
|
|
152
|
-
it('
|
|
189
|
+
it('removes dialog from DOM when modelValue becomes false', async () => {
|
|
153
190
|
const wrapper = mount(Modal, {
|
|
191
|
+
...globalConfig,
|
|
154
192
|
props: { modelValue: true }
|
|
155
193
|
})
|
|
156
194
|
|
|
195
|
+
expect(wrapper.find('dialog').exists()).toBe(true)
|
|
157
196
|
await wrapper.setProps({ modelValue: false })
|
|
158
|
-
|
|
159
|
-
expect(closeMock).toHaveBeenCalled()
|
|
197
|
+
expect(wrapper.find('dialog').exists()).toBe(false)
|
|
160
198
|
})
|
|
161
199
|
|
|
162
200
|
it('locks body scroll when opened', async () => {
|
|
163
201
|
const wrapper = mount(Modal, {
|
|
202
|
+
...globalConfig,
|
|
164
203
|
props: { modelValue: false }
|
|
165
204
|
})
|
|
166
205
|
|
|
@@ -171,6 +210,7 @@ describe('Modal', () => {
|
|
|
171
210
|
|
|
172
211
|
it('unlocks body scroll when closed', async () => {
|
|
173
212
|
const wrapper = mount(Modal, {
|
|
213
|
+
...globalConfig,
|
|
174
214
|
props: { modelValue: true }
|
|
175
215
|
})
|
|
176
216
|
|
|
@@ -183,6 +223,7 @@ describe('Modal', () => {
|
|
|
183
223
|
describe('Close button', () => {
|
|
184
224
|
it('renders close button when header is visible', () => {
|
|
185
225
|
const wrapper = mount(Modal, {
|
|
226
|
+
...globalConfig,
|
|
186
227
|
props: { modelValue: true, title: 'Test' }
|
|
187
228
|
})
|
|
188
229
|
expect(wrapper.find('.ui-modal__close').exists()).toBe(true)
|
|
@@ -190,6 +231,7 @@ describe('Modal', () => {
|
|
|
190
231
|
|
|
191
232
|
it('emits update:modelValue false when close button clicked', async () => {
|
|
192
233
|
const wrapper = mount(Modal, {
|
|
234
|
+
...globalConfig,
|
|
193
235
|
props: { modelValue: true, title: 'Test' }
|
|
194
236
|
})
|
|
195
237
|
|
|
@@ -200,6 +242,7 @@ describe('Modal', () => {
|
|
|
200
242
|
|
|
201
243
|
it('has aria-label', () => {
|
|
202
244
|
const wrapper = mount(Modal, {
|
|
245
|
+
...globalConfig,
|
|
203
246
|
props: { modelValue: true, title: 'Test' }
|
|
204
247
|
})
|
|
205
248
|
expect(wrapper.find('.ui-modal__close').attributes('aria-label')).toBe('Close modal')
|
|
@@ -209,6 +252,7 @@ describe('Modal', () => {
|
|
|
209
252
|
describe('Backdrop click', () => {
|
|
210
253
|
it('emits update:modelValue false when backdrop clicked', async () => {
|
|
211
254
|
const wrapper = mount(Modal, {
|
|
255
|
+
...globalConfig,
|
|
212
256
|
props: { modelValue: true, title: 'Test' }
|
|
213
257
|
})
|
|
214
258
|
|
|
@@ -221,6 +265,7 @@ describe('Modal', () => {
|
|
|
221
265
|
|
|
222
266
|
it('does not close when content clicked', async () => {
|
|
223
267
|
const wrapper = mount(Modal, {
|
|
268
|
+
...globalConfig,
|
|
224
269
|
props: { modelValue: true, title: 'Test' }
|
|
225
270
|
})
|
|
226
271
|
|
|
@@ -231,6 +276,7 @@ describe('Modal', () => {
|
|
|
231
276
|
|
|
232
277
|
it('does not close on backdrop click when persistent', async () => {
|
|
233
278
|
const wrapper = mount(Modal, {
|
|
279
|
+
...globalConfig,
|
|
234
280
|
props: { modelValue: true, title: 'Test', persistent: true }
|
|
235
281
|
})
|
|
236
282
|
|
|
@@ -244,6 +290,7 @@ describe('Modal', () => {
|
|
|
244
290
|
describe('Escape key', () => {
|
|
245
291
|
it('emits update:modelValue false on cancel event', async () => {
|
|
246
292
|
const wrapper = mount(Modal, {
|
|
293
|
+
...globalConfig,
|
|
247
294
|
props: { modelValue: true, title: 'Test' }
|
|
248
295
|
})
|
|
249
296
|
|
|
@@ -254,6 +301,7 @@ describe('Modal', () => {
|
|
|
254
301
|
|
|
255
302
|
it('prevents default on cancel when persistent', async () => {
|
|
256
303
|
const wrapper = mount(Modal, {
|
|
304
|
+
...globalConfig,
|
|
257
305
|
props: { modelValue: true, title: 'Test', persistent: true }
|
|
258
306
|
})
|
|
259
307
|
|
|
@@ -269,6 +317,7 @@ describe('Modal', () => {
|
|
|
269
317
|
describe('Close event', () => {
|
|
270
318
|
it('emits close event on dialog close', async () => {
|
|
271
319
|
const wrapper = mount(Modal, {
|
|
320
|
+
...globalConfig,
|
|
272
321
|
props: { modelValue: true, title: 'Test' }
|
|
273
322
|
})
|
|
274
323
|
|
|
@@ -279,6 +328,7 @@ describe('Modal', () => {
|
|
|
279
328
|
|
|
280
329
|
it('syncs modelValue on dialog close', async () => {
|
|
281
330
|
const wrapper = mount(Modal, {
|
|
331
|
+
...globalConfig,
|
|
282
332
|
props: { modelValue: true, title: 'Test' }
|
|
283
333
|
})
|
|
284
334
|
|
|
@@ -289,13 +339,17 @@ describe('Modal', () => {
|
|
|
289
339
|
})
|
|
290
340
|
|
|
291
341
|
describe('Accessibility', () => {
|
|
292
|
-
it('uses native dialog element', () => {
|
|
293
|
-
const wrapper = mount(Modal
|
|
342
|
+
it('uses native dialog element when open', () => {
|
|
343
|
+
const wrapper = mount(Modal, {
|
|
344
|
+
...globalConfig,
|
|
345
|
+
props: { modelValue: true }
|
|
346
|
+
})
|
|
294
347
|
expect(wrapper.find('dialog').exists()).toBe(true)
|
|
295
348
|
})
|
|
296
349
|
|
|
297
350
|
it('close button has aria-label', () => {
|
|
298
351
|
const wrapper = mount(Modal, {
|
|
352
|
+
...globalConfig,
|
|
299
353
|
props: { modelValue: true, title: 'Test' }
|
|
300
354
|
})
|
|
301
355
|
expect(wrapper.find('.ui-modal__close').attributes('aria-label')).toBe('Close modal')
|
|
@@ -303,6 +357,7 @@ describe('Modal', () => {
|
|
|
303
357
|
|
|
304
358
|
it('close button icon is aria-hidden', () => {
|
|
305
359
|
const wrapper = mount(Modal, {
|
|
360
|
+
...globalConfig,
|
|
306
361
|
props: { modelValue: true, title: 'Test' }
|
|
307
362
|
})
|
|
308
363
|
expect(wrapper.find('.ui-modal__close svg').attributes('aria-hidden')).toBe('true')
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed, watch, onMounted, useSlots } from 'vue'
|
|
2
|
+
import { ref, computed, watch, onMounted, nextTick, useSlots } from 'vue'
|
|
3
3
|
import { useScrollLock } from '../../composables'
|
|
4
|
+
import type { ClassValue } from '../../types/common'
|
|
4
5
|
|
|
5
6
|
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
|
6
7
|
|
|
7
8
|
export interface ModalProps {
|
|
9
|
+
/** Additional CSS classes */
|
|
10
|
+
class?: ClassValue
|
|
8
11
|
/** Controls visibility (v-model) */
|
|
9
12
|
modelValue?: boolean
|
|
10
13
|
/** Optional header title */
|
|
@@ -57,27 +60,23 @@ function handleClose() {
|
|
|
57
60
|
emit('close')
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (open && !dialog.open) {
|
|
65
|
-
dialog.showModal()
|
|
66
|
-
} else if (!open && dialog.open) {
|
|
67
|
-
dialog.close()
|
|
68
|
-
}
|
|
63
|
+
async function openDialog() {
|
|
64
|
+
await nextTick()
|
|
65
|
+
dialogRef.value?.showModal()
|
|
69
66
|
}
|
|
70
67
|
|
|
71
68
|
onMounted(() => {
|
|
72
69
|
if (props.modelValue) {
|
|
73
|
-
|
|
70
|
+
openDialog()
|
|
74
71
|
}
|
|
75
72
|
})
|
|
76
73
|
|
|
77
74
|
watch(
|
|
78
75
|
() => props.modelValue,
|
|
79
76
|
(open) => {
|
|
80
|
-
|
|
77
|
+
if (open) {
|
|
78
|
+
openDialog()
|
|
79
|
+
}
|
|
81
80
|
}
|
|
82
81
|
)
|
|
83
82
|
|
|
@@ -85,48 +84,50 @@ const hasHeader = () => props.title || slots.header
|
|
|
85
84
|
</script>
|
|
86
85
|
|
|
87
86
|
<template>
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<svg
|
|
108
|
-
viewBox="0 0 24 24"
|
|
109
|
-
fill="none"
|
|
110
|
-
stroke="currentColor"
|
|
111
|
-
stroke-width="2"
|
|
112
|
-
stroke-linecap="round"
|
|
113
|
-
stroke-linejoin="round"
|
|
114
|
-
aria-hidden="true"
|
|
87
|
+
<Teleport to="body">
|
|
88
|
+
<dialog
|
|
89
|
+
v-if="modelValue"
|
|
90
|
+
ref="dialogRef"
|
|
91
|
+
:class="[props.class, 'ui-modal', `ui-modal--${size}`]"
|
|
92
|
+
@click="handleBackdropClick"
|
|
93
|
+
@cancel="handleCancel"
|
|
94
|
+
@close="handleClose"
|
|
95
|
+
>
|
|
96
|
+
<div class="ui-modal__box">
|
|
97
|
+
<header v-if="hasHeader()" class="ui-modal__header">
|
|
98
|
+
<slot name="header">
|
|
99
|
+
<h3 class="ui-modal__title">{{ title }}</h3>
|
|
100
|
+
</slot>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
class="ui-modal__close"
|
|
104
|
+
aria-label="Close modal"
|
|
105
|
+
@click="close"
|
|
115
106
|
>
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
107
|
+
<svg
|
|
108
|
+
viewBox="0 0 24 24"
|
|
109
|
+
fill="none"
|
|
110
|
+
stroke="currentColor"
|
|
111
|
+
stroke-width="2"
|
|
112
|
+
stroke-linecap="round"
|
|
113
|
+
stroke-linejoin="round"
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
>
|
|
116
|
+
<path d="M6 18L18 6M6 6l12 12" />
|
|
117
|
+
</svg>
|
|
118
|
+
</button>
|
|
119
|
+
</header>
|
|
120
|
+
|
|
121
|
+
<div class="ui-modal__body">
|
|
122
|
+
<slot />
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<footer v-if="$slots.footer" class="ui-modal__footer">
|
|
126
|
+
<slot name="footer" />
|
|
127
|
+
</footer>
|
|
123
128
|
</div>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<slot name="footer" />
|
|
127
|
-
</footer>
|
|
128
|
-
</div>
|
|
129
|
-
</dialog>
|
|
129
|
+
</dialog>
|
|
130
|
+
</Teleport>
|
|
130
131
|
</template>
|
|
131
132
|
|
|
132
133
|
<style scoped>
|
|
@@ -283,54 +284,56 @@ const hasHeader = () => props.title || slots.header
|
|
|
283
284
|
</style>
|
|
284
285
|
|
|
285
286
|
<style>
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
.ui-modal::backdrop {
|
|
295
|
-
background-color: transparent;
|
|
296
|
-
backdrop-filter: blur(0px);
|
|
297
|
-
transition:
|
|
298
|
-
background-color var(--duration-slow) var(--ease-default),
|
|
299
|
-
backdrop-filter var(--duration-slow) var(--ease-default),
|
|
300
|
-
overlay var(--duration-slow) allow-discrete,
|
|
301
|
-
display var(--duration-slow) allow-discrete;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
.ui-modal[open]::backdrop {
|
|
305
|
-
background-color: var(--modal-backdrop);
|
|
306
|
-
backdrop-filter: blur(2px);
|
|
307
|
-
}
|
|
287
|
+
@layer spire-components {
|
|
288
|
+
.ui-modal {
|
|
289
|
+
transition:
|
|
290
|
+
opacity var(--duration-slow) var(--ease-default),
|
|
291
|
+
transform var(--duration-slow) var(--ease-out-expo),
|
|
292
|
+
overlay var(--duration-slow) var(--ease-default) allow-discrete,
|
|
293
|
+
display var(--duration-slow) var(--ease-default) allow-discrete;
|
|
294
|
+
}
|
|
308
295
|
|
|
309
|
-
|
|
310
|
-
.ui-modal[open]::backdrop {
|
|
296
|
+
.ui-modal::backdrop {
|
|
311
297
|
background-color: transparent;
|
|
312
298
|
backdrop-filter: blur(0px);
|
|
299
|
+
transition:
|
|
300
|
+
background-color var(--duration-slow) var(--ease-default),
|
|
301
|
+
backdrop-filter var(--duration-slow) var(--ease-default),
|
|
302
|
+
overlay var(--duration-slow) allow-discrete,
|
|
303
|
+
display var(--duration-slow) allow-discrete;
|
|
313
304
|
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
.ui-modal .ui-modal__box {
|
|
317
|
-
opacity: 0;
|
|
318
|
-
transform: scale(0.95) translateY(10px);
|
|
319
305
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
306
|
+
.ui-modal[open]::backdrop {
|
|
307
|
+
background-color: var(--modal-backdrop);
|
|
308
|
+
backdrop-filter: blur(2px);
|
|
309
|
+
}
|
|
324
310
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
311
|
+
@starting-style {
|
|
312
|
+
.ui-modal[open]::backdrop {
|
|
313
|
+
background-color: transparent;
|
|
314
|
+
backdrop-filter: blur(0px);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
329
317
|
|
|
330
|
-
|
|
331
|
-
.ui-modal[open] .ui-modal__box {
|
|
318
|
+
.ui-modal .ui-modal__box {
|
|
332
319
|
opacity: 0;
|
|
333
320
|
transform: scale(0.95) translateY(10px);
|
|
321
|
+
|
|
322
|
+
transition:
|
|
323
|
+
opacity var(--duration-slow) var(--ease-default),
|
|
324
|
+
transform var(--duration-slow) var(--ease-out-expo);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.ui-modal[open] .ui-modal__box {
|
|
328
|
+
opacity: 1;
|
|
329
|
+
transform: scale(1) translateY(0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
@starting-style {
|
|
333
|
+
.ui-modal[open] .ui-modal__box {
|
|
334
|
+
opacity: 0;
|
|
335
|
+
transform: scale(0.95) translateY(10px);
|
|
336
|
+
}
|
|
334
337
|
}
|
|
335
338
|
}
|
|
336
339
|
</style>
|
|
@@ -3,11 +3,14 @@ import { computed } from 'vue'
|
|
|
3
3
|
import Button from '../Button/Button.vue'
|
|
4
4
|
import { useInternalIcon } from '../../config/icons'
|
|
5
5
|
import { generatePageRange, type PageItem } from './utils'
|
|
6
|
+
import type { ClassValue } from '../../types/common'
|
|
6
7
|
|
|
7
8
|
const ChevronLeftIcon = useInternalIcon('chevronLeft')
|
|
8
9
|
const ChevronRightIcon = useInternalIcon('chevronRight')
|
|
9
10
|
|
|
10
11
|
export interface PaginationProps {
|
|
12
|
+
/** Additional CSS classes */
|
|
13
|
+
class?: ClassValue
|
|
11
14
|
/** Current active page (v-model) */
|
|
12
15
|
modelValue: number
|
|
13
16
|
/** Total count of items */
|
|
@@ -86,8 +89,9 @@ const buttonSize = computed(() => {
|
|
|
86
89
|
|
|
87
90
|
<template>
|
|
88
91
|
<nav
|
|
89
|
-
class="ui-pagination"
|
|
90
92
|
:class="[
|
|
93
|
+
props.class,
|
|
94
|
+
'ui-pagination',
|
|
91
95
|
`ui-pagination--${size}`,
|
|
92
96
|
{ 'ui-pagination--disabled': disabled }
|
|
93
97
|
]"
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
onUnmounted
|
|
9
9
|
} from 'vue'
|
|
10
10
|
import { useId, useScrollLock } from '../../composables'
|
|
11
|
+
import type { ClassValue } from '../../types/common'
|
|
11
12
|
|
|
12
13
|
export type PopoverPlacement =
|
|
13
14
|
| 'top' | 'top-start' | 'top-end'
|
|
@@ -16,6 +17,8 @@ export type PopoverPlacement =
|
|
|
16
17
|
| 'right' | 'right-start' | 'right-end'
|
|
17
18
|
|
|
18
19
|
export interface PopoverProps {
|
|
20
|
+
/** Additional CSS classes */
|
|
21
|
+
class?: ClassValue
|
|
19
22
|
/** Popover placement relative to trigger */
|
|
20
23
|
placement?: PopoverPlacement
|
|
21
24
|
/** Offset from trigger (px) */
|
|
@@ -305,7 +308,7 @@ defineExpose({
|
|
|
305
308
|
</script>
|
|
306
309
|
|
|
307
310
|
<template>
|
|
308
|
-
<div class="ui-popover">
|
|
311
|
+
<div :class="[props.class, 'ui-popover']">
|
|
309
312
|
<div
|
|
310
313
|
ref="triggerRef"
|
|
311
314
|
class="ui-popover__trigger"
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
+
import type { ClassValue } from '../../types/common'
|
|
3
4
|
|
|
4
5
|
export type ProgressVariant = 'linear' | 'circular'
|
|
5
6
|
export type ProgressSize = 'sm' | 'md' | 'lg'
|
|
6
7
|
export type ProgressColor = 'primary' | 'success' | 'warning' | 'error'
|
|
7
8
|
|
|
8
9
|
export interface ProgressProps {
|
|
10
|
+
/** Additional CSS classes */
|
|
11
|
+
class?: ClassValue
|
|
9
12
|
/** Current progress value (0-100) */
|
|
10
13
|
value?: number
|
|
11
14
|
/** Display variant */
|
|
@@ -42,6 +45,7 @@ const props = withDefaults(defineProps<ProgressProps>(), {
|
|
|
42
45
|
const clampedValue = computed(() => Math.min(100, Math.max(0, props.value)))
|
|
43
46
|
|
|
44
47
|
const linearClasses = computed(() => [
|
|
48
|
+
props.class,
|
|
45
49
|
'ui-progress-linear',
|
|
46
50
|
`ui-progress-linear--${props.size}`,
|
|
47
51
|
`ui-progress-linear--${props.color}`,
|
|
@@ -53,6 +57,7 @@ const linearClasses = computed(() => [
|
|
|
53
57
|
])
|
|
54
58
|
|
|
55
59
|
const circularClasses = computed(() => [
|
|
60
|
+
props.class,
|
|
56
61
|
'ui-progress-circular',
|
|
57
62
|
`ui-progress-circular--${props.size}`,
|
|
58
63
|
`ui-progress-circular--${props.color}`,
|
|
@@ -213,4 +213,92 @@ describe('Radio', () => {
|
|
|
213
213
|
expect(wrapper.find('.ui-radio__box').attributes('aria-hidden')).toBe('true')
|
|
214
214
|
})
|
|
215
215
|
})
|
|
216
|
+
|
|
217
|
+
describe('Pill variant', () => {
|
|
218
|
+
it('renders pill structure when variant is pill', () => {
|
|
219
|
+
const wrapper = mount(Radio, {
|
|
220
|
+
props: { value: 'a', variant: 'pill', label: 'Option A' }
|
|
221
|
+
})
|
|
222
|
+
expect(wrapper.find('.ui-radio-pill').exists()).toBe(true)
|
|
223
|
+
expect(wrapper.find('.ui-radio').exists()).toBe(false)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('renders default structure when variant is default', () => {
|
|
227
|
+
const wrapper = mount(Radio, {
|
|
228
|
+
props: { value: 'a', variant: 'default' }
|
|
229
|
+
})
|
|
230
|
+
expect(wrapper.find('.ui-radio').exists()).toBe(true)
|
|
231
|
+
expect(wrapper.find('.ui-radio-pill').exists()).toBe(false)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('defaults to default variant', () => {
|
|
235
|
+
const wrapper = mount(Radio, {
|
|
236
|
+
props: { value: 'a' }
|
|
237
|
+
})
|
|
238
|
+
expect(wrapper.find('.ui-radio').exists()).toBe(true)
|
|
239
|
+
expect(wrapper.find('.ui-radio-pill').exists()).toBe(false)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('renders pill label', () => {
|
|
243
|
+
const wrapper = mount(Radio, {
|
|
244
|
+
props: { value: 'a', variant: 'pill', label: 'Option A' }
|
|
245
|
+
})
|
|
246
|
+
expect(wrapper.find('.ui-radio-pill__label').text()).toBe('Option A')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('renders pill check indicator', () => {
|
|
250
|
+
const wrapper = mount(Radio, {
|
|
251
|
+
props: { value: 'a', variant: 'pill', label: 'Option A' }
|
|
252
|
+
})
|
|
253
|
+
expect(wrapper.find('.ui-radio-pill__indicator').exists()).toBe(true)
|
|
254
|
+
expect(wrapper.find('.ui-radio-pill__check').exists()).toBe(true)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('applies checked class when checked', () => {
|
|
258
|
+
const wrapper = mount(Radio, {
|
|
259
|
+
props: { value: 'a', modelValue: 'a', variant: 'pill', label: 'Option A' }
|
|
260
|
+
})
|
|
261
|
+
expect(wrapper.find('.ui-radio-pill').classes()).toContain('ui-radio-pill--checked')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('does not apply checked class when unchecked', () => {
|
|
265
|
+
const wrapper = mount(Radio, {
|
|
266
|
+
props: { value: 'a', modelValue: 'b', variant: 'pill', label: 'Option A' }
|
|
267
|
+
})
|
|
268
|
+
expect(wrapper.find('.ui-radio-pill').classes()).not.toContain('ui-radio-pill--checked')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('applies disabled class when disabled', () => {
|
|
272
|
+
const wrapper = mount(Radio, {
|
|
273
|
+
props: { value: 'a', variant: 'pill', label: 'Option A', disabled: true }
|
|
274
|
+
})
|
|
275
|
+
expect(wrapper.find('.ui-radio-pill').classes()).toContain('ui-radio-pill--disabled')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('emits events on pill change', async () => {
|
|
279
|
+
const wrapper = mount(Radio, {
|
|
280
|
+
props: { value: 'a', modelValue: 'b', variant: 'pill', label: 'Option A' }
|
|
281
|
+
})
|
|
282
|
+
await wrapper.find('input').trigger('change')
|
|
283
|
+
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['a'])
|
|
284
|
+
expect(wrapper.emitted('change')?.[0]).toEqual(['a'])
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const sizes = ['sm', 'md', 'lg'] as const
|
|
288
|
+
sizes.forEach(size => {
|
|
289
|
+
it(`applies ${size} size class to pill`, () => {
|
|
290
|
+
const wrapper = mount(Radio, {
|
|
291
|
+
props: { value: 'a', variant: 'pill', label: 'Option A', size }
|
|
292
|
+
})
|
|
293
|
+
expect(wrapper.find('.ui-radio-pill').classes()).toContain(`ui-radio-pill--${size}`)
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('pill indicator has aria-hidden', () => {
|
|
298
|
+
const wrapper = mount(Radio, {
|
|
299
|
+
props: { value: 'a', variant: 'pill', label: 'Option A' }
|
|
300
|
+
})
|
|
301
|
+
expect(wrapper.find('.ui-radio-pill__indicator').attributes('aria-hidden')).toBe('true')
|
|
302
|
+
})
|
|
303
|
+
})
|
|
216
304
|
})
|