@htlkg/components 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 (79) hide show
  1. package/dist/composables/index.js +388 -0
  2. package/dist/composables/index.js.map +1 -0
  3. package/package.json +41 -0
  4. package/src/composables/index.ts +6 -0
  5. package/src/composables/useForm.test.ts +229 -0
  6. package/src/composables/useForm.ts +130 -0
  7. package/src/composables/useFormValidation.test.ts +189 -0
  8. package/src/composables/useFormValidation.ts +83 -0
  9. package/src/composables/useModal.property.test.ts +164 -0
  10. package/src/composables/useModal.ts +43 -0
  11. package/src/composables/useNotifications.test.ts +166 -0
  12. package/src/composables/useNotifications.ts +81 -0
  13. package/src/composables/useTable.property.test.ts +198 -0
  14. package/src/composables/useTable.ts +134 -0
  15. package/src/composables/useTabs.property.test.ts +247 -0
  16. package/src/composables/useTabs.ts +101 -0
  17. package/src/data/Chart.demo.vue +340 -0
  18. package/src/data/Chart.md +525 -0
  19. package/src/data/Chart.vue +133 -0
  20. package/src/data/DataList.md +80 -0
  21. package/src/data/DataList.test.ts +69 -0
  22. package/src/data/DataList.vue +46 -0
  23. package/src/data/SearchableSelect.md +107 -0
  24. package/src/data/SearchableSelect.vue +124 -0
  25. package/src/data/Table.demo.vue +296 -0
  26. package/src/data/Table.md +588 -0
  27. package/src/data/Table.property.test.ts +548 -0
  28. package/src/data/Table.test.ts +562 -0
  29. package/src/data/Table.unit.test.ts +544 -0
  30. package/src/data/Table.vue +321 -0
  31. package/src/data/index.ts +5 -0
  32. package/src/domain/BrandCard.md +81 -0
  33. package/src/domain/BrandCard.vue +63 -0
  34. package/src/domain/BrandSelector.md +84 -0
  35. package/src/domain/BrandSelector.vue +65 -0
  36. package/src/domain/ProductBadge.md +60 -0
  37. package/src/domain/ProductBadge.vue +47 -0
  38. package/src/domain/UserAvatar.md +84 -0
  39. package/src/domain/UserAvatar.vue +60 -0
  40. package/src/domain/domain-components.property.test.ts +449 -0
  41. package/src/domain/index.ts +4 -0
  42. package/src/forms/DateRange.demo.vue +273 -0
  43. package/src/forms/DateRange.md +337 -0
  44. package/src/forms/DateRange.vue +110 -0
  45. package/src/forms/JsonSchemaForm.demo.vue +549 -0
  46. package/src/forms/JsonSchemaForm.md +112 -0
  47. package/src/forms/JsonSchemaForm.property.test.ts +817 -0
  48. package/src/forms/JsonSchemaForm.test.ts +601 -0
  49. package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
  50. package/src/forms/JsonSchemaForm.vue +615 -0
  51. package/src/forms/index.ts +3 -0
  52. package/src/index.ts +17 -0
  53. package/src/navigation/Breadcrumbs.demo.vue +142 -0
  54. package/src/navigation/Breadcrumbs.md +102 -0
  55. package/src/navigation/Breadcrumbs.test.ts +69 -0
  56. package/src/navigation/Breadcrumbs.vue +58 -0
  57. package/src/navigation/Stepper.demo.vue +337 -0
  58. package/src/navigation/Stepper.md +174 -0
  59. package/src/navigation/Stepper.vue +146 -0
  60. package/src/navigation/Tabs.demo.vue +293 -0
  61. package/src/navigation/Tabs.md +163 -0
  62. package/src/navigation/Tabs.test.ts +176 -0
  63. package/src/navigation/Tabs.vue +104 -0
  64. package/src/navigation/index.ts +5 -0
  65. package/src/overlays/Alert.demo.vue +377 -0
  66. package/src/overlays/Alert.md +248 -0
  67. package/src/overlays/Alert.test.ts +166 -0
  68. package/src/overlays/Alert.vue +70 -0
  69. package/src/overlays/Drawer.md +140 -0
  70. package/src/overlays/Drawer.test.ts +92 -0
  71. package/src/overlays/Drawer.vue +76 -0
  72. package/src/overlays/Modal.demo.vue +149 -0
  73. package/src/overlays/Modal.md +385 -0
  74. package/src/overlays/Modal.test.ts +128 -0
  75. package/src/overlays/Modal.vue +86 -0
  76. package/src/overlays/Notification.md +150 -0
  77. package/src/overlays/Notification.test.ts +96 -0
  78. package/src/overlays/Notification.vue +58 -0
  79. package/src/overlays/index.ts +4 -0
@@ -0,0 +1,293 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue';
3
+ import { Tabs } from '@htlkg/components/navigation';
4
+
5
+ // Active tab state
6
+ const activeTab = ref('profile');
7
+
8
+ // Interactive controls
9
+ const showBadges = ref(true);
10
+ const tabCount = ref(4);
11
+
12
+ // All available tabs
13
+ const allTabs = [
14
+ { id: 'profile', label: 'Profile', count: 0 },
15
+ { id: 'settings', label: 'Settings', count: 3 },
16
+ { id: 'notifications', label: 'Notifications', count: 12 },
17
+ { id: 'security', label: 'Security', count: 0 },
18
+ { id: 'billing', label: 'Billing', count: 1 },
19
+ { id: 'team', label: 'Team', count: 5 }
20
+ ];
21
+
22
+ // Tab configuration (computed based on controls)
23
+ const tabs = computed(() => {
24
+ const selectedTabs = allTabs.slice(0, tabCount.value);
25
+ return showBadges.value
26
+ ? selectedTabs
27
+ : selectedTabs.map(tab => ({ ...tab, count: undefined }));
28
+ });
29
+
30
+ // Sample data for each tab
31
+ const profileData = {
32
+ name: 'John Doe',
33
+ email: 'john.doe@example.com',
34
+ role: 'Administrator',
35
+ department: 'Engineering',
36
+ joinDate: 'January 15, 2023'
37
+ };
38
+
39
+ const settingsData = [
40
+ { id: 1, setting: 'Email Notifications', enabled: true },
41
+ { id: 2, setting: 'Push Notifications', enabled: false },
42
+ { id: 3, setting: 'SMS Alerts', enabled: true }
43
+ ];
44
+
45
+ const notificationsData = [
46
+ { id: 1, message: 'New user registered', time: '5 minutes ago', read: false },
47
+ { id: 2, message: 'System update completed', time: '1 hour ago', read: false },
48
+ { id: 3, message: 'Backup completed successfully', time: '2 hours ago', read: true },
49
+ { id: 4, message: 'New comment on your post', time: '3 hours ago', read: false },
50
+ { id: 5, message: 'Weekly report is ready', time: '1 day ago', read: true }
51
+ ];
52
+
53
+ const securityData = {
54
+ lastLogin: 'December 9, 2024 at 10:30 AM',
55
+ twoFactorEnabled: true,
56
+ activeSessions: 2,
57
+ passwordLastChanged: 'November 1, 2024'
58
+ };
59
+
60
+ const handleTabChange = (tabId: string) => {
61
+ console.log('Tab changed to:', tabId);
62
+ };
63
+ </script>
64
+
65
+ <template>
66
+ <div>
67
+ <!-- Interactive Controls Panel -->
68
+ <div class="mb-6 p-4 bg-purple-50 border border-purple-200 rounded-lg">
69
+ <h4 class="text-sm font-semibold text-purple-900 mb-3">🎮 Interactive Controls</h4>
70
+
71
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
72
+ <!-- Tab Count Control -->
73
+ <div>
74
+ <label class="block text-sm font-medium text-gray-700 mb-2">
75
+ Number of Tabs: {{ tabCount }}
76
+ </label>
77
+ <input
78
+ type="range"
79
+ v-model.number="tabCount"
80
+ min="2"
81
+ max="6"
82
+ class="w-full"
83
+ />
84
+ <div class="flex justify-between text-xs text-gray-500 mt-1">
85
+ <span>2</span>
86
+ <span>6</span>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Badge Toggle -->
91
+ <div>
92
+ <label class="block text-sm font-medium text-gray-700 mb-2">
93
+ Display Options:
94
+ </label>
95
+ <button
96
+ @click="showBadges = !showBadges"
97
+ :class="[
98
+ 'px-3 py-2 text-sm rounded-md transition-colors',
99
+ showBadges
100
+ ? 'bg-purple-600 text-white'
101
+ : 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'
102
+ ]"
103
+ >
104
+ {{ showBadges ? '🔢 Badges ON' : '🔢 Badges OFF' }}
105
+ </button>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Stats Display -->
111
+ <div class="mb-6 p-4 bg-blue-50 rounded border border-blue-200">
112
+ <div class="grid grid-cols-2 gap-4 text-sm">
113
+ <div>
114
+ <span class="font-medium text-gray-700">Active Tab:</span>
115
+ <span class="ml-2 text-blue-600 font-semibold">{{ activeTab }}</span>
116
+ </div>
117
+ <div>
118
+ <span class="font-medium text-gray-700">Total Tabs:</span>
119
+ <span class="ml-2 text-blue-600 font-semibold">{{ tabs.length }}</span>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- Tabs Component -->
125
+ <Tabs v-model="activeTab" :tabs="tabs" @tab-change="handleTabChange">
126
+ <!-- Profile Tab -->
127
+ <template #profile>
128
+ <div class="p-6 bg-white rounded-lg border">
129
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">Profile Information</h3>
130
+ <div class="space-y-3">
131
+ <div class="flex justify-between py-2 border-b">
132
+ <span class="text-gray-600">Name:</span>
133
+ <span class="font-medium text-gray-900">{{ profileData.name }}</span>
134
+ </div>
135
+ <div class="flex justify-between py-2 border-b">
136
+ <span class="text-gray-600">Email:</span>
137
+ <span class="font-medium text-gray-900">{{ profileData.email }}</span>
138
+ </div>
139
+ <div class="flex justify-between py-2 border-b">
140
+ <span class="text-gray-600">Role:</span>
141
+ <span class="font-medium text-gray-900">{{ profileData.role }}</span>
142
+ </div>
143
+ <div class="flex justify-between py-2 border-b">
144
+ <span class="text-gray-600">Department:</span>
145
+ <span class="font-medium text-gray-900">{{ profileData.department }}</span>
146
+ </div>
147
+ <div class="flex justify-between py-2">
148
+ <span class="text-gray-600">Join Date:</span>
149
+ <span class="font-medium text-gray-900">{{ profileData.joinDate }}</span>
150
+ </div>
151
+ </div>
152
+ <div class="mt-6">
153
+ <button
154
+ class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
155
+ >
156
+ Edit Profile
157
+ </button>
158
+ </div>
159
+ </div>
160
+ </template>
161
+
162
+ <!-- Settings Tab -->
163
+ <template #settings>
164
+ <div class="p-6 bg-white rounded-lg border">
165
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">Notification Settings</h3>
166
+ <div class="space-y-4">
167
+ <div
168
+ v-for="setting in settingsData"
169
+ :key="setting.id"
170
+ class="flex items-center justify-between p-4 bg-gray-50 rounded"
171
+ >
172
+ <span class="text-gray-900">{{ setting.setting }}</span>
173
+ <label class="relative inline-flex items-center cursor-pointer">
174
+ <input
175
+ type="checkbox"
176
+ :checked="setting.enabled"
177
+ class="sr-only peer"
178
+ />
179
+ <div
180
+ class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"
181
+ ></div>
182
+ </label>
183
+ </div>
184
+ </div>
185
+ <div class="mt-6">
186
+ <button
187
+ class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
188
+ >
189
+ Save Settings
190
+ </button>
191
+ </div>
192
+ </div>
193
+ </template>
194
+
195
+ <!-- Notifications Tab -->
196
+ <template #notifications>
197
+ <div class="p-6 bg-white rounded-lg border">
198
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">Recent Notifications</h3>
199
+ <div class="space-y-2">
200
+ <div
201
+ v-for="notification in notificationsData"
202
+ :key="notification.id"
203
+ class="flex items-start justify-between p-4 rounded transition-colors"
204
+ :class="notification.read ? 'bg-gray-50' : 'bg-blue-50'"
205
+ >
206
+ <div class="flex-1">
207
+ <p
208
+ class="text-sm"
209
+ :class="notification.read ? 'text-gray-600' : 'text-gray-900 font-medium'"
210
+ >
211
+ {{ notification.message }}
212
+ </p>
213
+ <p class="text-xs text-gray-500 mt-1">{{ notification.time }}</p>
214
+ </div>
215
+ <span
216
+ v-if="!notification.read"
217
+ class="ml-4 w-2 h-2 bg-blue-600 rounded-full"
218
+ ></span>
219
+ </div>
220
+ </div>
221
+ <div class="mt-6 flex gap-2">
222
+ <button
223
+ class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
224
+ >
225
+ Mark All as Read
226
+ </button>
227
+ <button
228
+ class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition-colors"
229
+ >
230
+ Clear All
231
+ </button>
232
+ </div>
233
+ </div>
234
+ </template>
235
+
236
+ <!-- Security Tab -->
237
+ <template #security>
238
+ <div class="p-6 bg-white rounded-lg border">
239
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">Security Settings</h3>
240
+ <div class="space-y-4">
241
+ <div class="p-4 bg-gray-50 rounded">
242
+ <div class="flex justify-between items-center mb-2">
243
+ <span class="font-medium text-gray-900">Last Login</span>
244
+ <span class="text-sm text-gray-600">{{ securityData.lastLogin }}</span>
245
+ </div>
246
+ </div>
247
+ <div class="p-4 bg-gray-50 rounded">
248
+ <div class="flex justify-between items-center mb-2">
249
+ <span class="font-medium text-gray-900">Two-Factor Authentication</span>
250
+ <span
251
+ class="px-2 py-1 text-xs rounded"
252
+ :class="
253
+ securityData.twoFactorEnabled
254
+ ? 'bg-green-100 text-green-800'
255
+ : 'bg-red-100 text-red-800'
256
+ "
257
+ >
258
+ {{ securityData.twoFactorEnabled ? 'Enabled' : 'Disabled' }}
259
+ </span>
260
+ </div>
261
+ </div>
262
+ <div class="p-4 bg-gray-50 rounded">
263
+ <div class="flex justify-between items-center mb-2">
264
+ <span class="font-medium text-gray-900">Active Sessions</span>
265
+ <span class="text-sm text-gray-600">{{ securityData.activeSessions }}</span>
266
+ </div>
267
+ </div>
268
+ <div class="p-4 bg-gray-50 rounded">
269
+ <div class="flex justify-between items-center mb-2">
270
+ <span class="font-medium text-gray-900">Password Last Changed</span>
271
+ <span class="text-sm text-gray-600">{{
272
+ securityData.passwordLastChanged
273
+ }}</span>
274
+ </div>
275
+ </div>
276
+ </div>
277
+ <div class="mt-6 flex gap-2">
278
+ <button
279
+ class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
280
+ >
281
+ Change Password
282
+ </button>
283
+ <button
284
+ class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition-colors"
285
+ >
286
+ View All Sessions
287
+ </button>
288
+ </div>
289
+ </div>
290
+ </template>
291
+ </Tabs>
292
+ </div>
293
+ </template>
@@ -0,0 +1,163 @@
1
+ # Tabs Component
2
+
3
+ A tabbed interface component for organizing content into separate views.
4
+
5
+ ## Features
6
+
7
+ - **v-model support**: Two-way binding for active tab
8
+ - **Badge counts**: Optional count badges on tabs
9
+ - **Slot-based content**: Named slots for each tab
10
+ - **Programmatic navigation**: Methods for tab control
11
+ - **Keyboard navigation**: Arrow keys for tab switching
12
+
13
+ ## Import
14
+
15
+ ```typescript
16
+ import { Tabs } from '@htlkg/components';
17
+ // or
18
+ import { Tabs } from '@htlkg/components/navigation';
19
+ ```
20
+
21
+ ## Props
22
+
23
+ | Prop | Type | Default | Description |
24
+ |------|------|---------|-------------|
25
+ | `tabs` | `Tab[]` | required | Tab definitions |
26
+ | `modelValue` | `string` | `''` | Active tab ID (v-model) |
27
+
28
+ ### Tab Interface
29
+
30
+ ```typescript
31
+ interface Tab {
32
+ id: string;
33
+ label: string;
34
+ count?: number;
35
+ }
36
+ ```
37
+
38
+ ## Events
39
+
40
+ | Event | Payload | Description |
41
+ |-------|---------|-------------|
42
+ | `update:modelValue` | `string` | Active tab changed (v-model) |
43
+ | `tab-change` | `string` | Tab changed |
44
+
45
+ ## Slots
46
+
47
+ Named slots for each tab using the tab's `id` as the slot name. Each slot receives:
48
+ - `active-tab`: The currently active tab ID
49
+
50
+ ## Exposed Methods
51
+
52
+ | Method | Description |
53
+ |--------|-------------|
54
+ | `setActiveTab(tabId)` | Set active tab |
55
+ | `getActiveTab()` | Get active tab ID |
56
+ | `nextTab()` | Navigate to next tab |
57
+ | `previousTab()` | Navigate to previous tab |
58
+
59
+ ## Usage Examples
60
+
61
+ ### Basic Tabs
62
+
63
+ ```vue
64
+ <script setup>
65
+ import { ref } from 'vue';
66
+ import { Tabs } from '@htlkg/components';
67
+
68
+ const activeTab = ref('profile');
69
+
70
+ const tabs = [
71
+ { id: 'profile', label: 'Profile' },
72
+ { id: 'settings', label: 'Settings' },
73
+ { id: 'notifications', label: 'Notifications' }
74
+ ];
75
+ </script>
76
+
77
+ <template>
78
+ <Tabs v-model="activeTab" :tabs="tabs">
79
+ <template #profile>
80
+ <div>Profile content here</div>
81
+ </template>
82
+
83
+ <template #settings>
84
+ <div>Settings content here</div>
85
+ </template>
86
+
87
+ <template #notifications>
88
+ <div>Notifications content here</div>
89
+ </template>
90
+ </Tabs>
91
+ </template>
92
+ ```
93
+
94
+ ### Tabs with Counts
95
+
96
+ ```vue
97
+ <script setup>
98
+ import { ref } from 'vue';
99
+ import { Tabs } from '@htlkg/components';
100
+
101
+ const activeTab = ref('all');
102
+
103
+ const tabs = [
104
+ { id: 'all', label: 'All', count: 24 },
105
+ { id: 'active', label: 'Active', count: 18 },
106
+ { id: 'archived', label: 'Archived', count: 6 }
107
+ ];
108
+ </script>
109
+
110
+ <template>
111
+ <Tabs v-model="activeTab" :tabs="tabs">
112
+ <template #all>
113
+ <div>All items (24)</div>
114
+ </template>
115
+
116
+ <template #active>
117
+ <div>Active items (18)</div>
118
+ </template>
119
+
120
+ <template #archived>
121
+ <div>Archived items (6)</div>
122
+ </template>
123
+ </Tabs>
124
+ </template>
125
+ ```
126
+
127
+ ### Programmatic Control
128
+
129
+ ```vue
130
+ <script setup>
131
+ import { ref } from 'vue';
132
+ import { Tabs } from '@htlkg/components';
133
+
134
+ const tabsRef = ref();
135
+ const activeTab = ref('tab1');
136
+
137
+ const tabs = [
138
+ { id: 'tab1', label: 'Tab 1' },
139
+ { id: 'tab2', label: 'Tab 2' },
140
+ { id: 'tab3', label: 'Tab 3' }
141
+ ];
142
+
143
+ const next = () => tabsRef.value?.nextTab();
144
+ const previous = () => tabsRef.value?.previousTab();
145
+ </script>
146
+
147
+ <template>
148
+ <Tabs ref="tabsRef" v-model="activeTab" :tabs="tabs">
149
+ <template #tab1>Content 1</template>
150
+ <template #tab2>Content 2</template>
151
+ <template #tab3>Content 3</template>
152
+ </Tabs>
153
+
154
+ <div class="mt-4 space-x-2">
155
+ <button @click="previous">Previous</button>
156
+ <button @click="next">Next</button>
157
+ </div>
158
+ </template>
159
+ ```
160
+
161
+ ## Demo
162
+
163
+ See the [Tabs demo page](/components/tabs) for interactive examples.
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import Tabs from './Tabs.vue';
4
+
5
+ describe('Tabs Component', () => {
6
+ const mockTabs = [
7
+ { id: 'tab1', label: 'Tab 1', count: 5 },
8
+ { id: 'tab2', label: 'Tab 2', count: 10 },
9
+ { id: 'tab3', label: 'Tab 3' }
10
+ ];
11
+
12
+ it('renders with basic props', () => {
13
+ const wrapper = mount(Tabs, {
14
+ props: {
15
+ tabs: mockTabs
16
+ }
17
+ });
18
+
19
+ expect(wrapper.exists()).toBe(true);
20
+ expect(wrapper.find('.tabs-wrapper').exists()).toBe(true);
21
+ });
22
+
23
+ it('supports v-model for activeTab', async () => {
24
+ const wrapper = mount(Tabs, {
25
+ props: {
26
+ tabs: mockTabs,
27
+ activeTab: 'tab1',
28
+ 'onUpdate:activeTab': (value: string) => wrapper.setProps({ activeTab: value })
29
+ }
30
+ });
31
+
32
+ expect(wrapper.props('activeTab')).toBe('tab1');
33
+
34
+ // Change active tab
35
+ const component = wrapper.vm as any;
36
+ component.handleTabClick('tab2');
37
+
38
+ await wrapper.vm.$nextTick();
39
+ expect(wrapper.emitted('update:activeTab')).toBeTruthy();
40
+ expect(wrapper.emitted('update:activeTab')?.[0]).toEqual(['tab2']);
41
+ });
42
+
43
+ it('emits tab-change event when tab is clicked', async () => {
44
+ const wrapper = mount(Tabs, {
45
+ props: {
46
+ tabs: mockTabs,
47
+ activeTab: 'tab1'
48
+ }
49
+ });
50
+
51
+ const component = wrapper.vm as any;
52
+ component.handleTabClick('tab2');
53
+
54
+ expect(wrapper.emitted('tab-change')).toBeTruthy();
55
+ expect(wrapper.emitted('tab-change')?.[0]).toEqual(['tab2']);
56
+ });
57
+
58
+ it('defaults to first tab when no activeTab is provided', () => {
59
+ const wrapper = mount(Tabs, {
60
+ props: {
61
+ tabs: mockTabs
62
+ }
63
+ });
64
+
65
+ const component = wrapper.vm as any;
66
+ expect(component.currentTab).toBe('tab1');
67
+ });
68
+
69
+ it('exposes setActiveTab method', () => {
70
+ const wrapper = mount(Tabs, {
71
+ props: {
72
+ tabs: mockTabs,
73
+ activeTab: 'tab1'
74
+ }
75
+ });
76
+
77
+ const component = wrapper.vm as any;
78
+ expect(component.setActiveTab).toBeDefined();
79
+ expect(typeof component.setActiveTab).toBe('function');
80
+
81
+ component.setActiveTab('tab3');
82
+ expect(wrapper.emitted('update:activeTab')).toBeTruthy();
83
+ });
84
+
85
+ it('exposes getActiveTab method', () => {
86
+ const wrapper = mount(Tabs, {
87
+ props: {
88
+ tabs: mockTabs,
89
+ activeTab: 'tab2'
90
+ }
91
+ });
92
+
93
+ const component = wrapper.vm as any;
94
+ expect(component.getActiveTab).toBeDefined();
95
+ expect(typeof component.getActiveTab).toBe('function');
96
+ expect(component.getActiveTab()).toBe('tab2');
97
+ });
98
+
99
+ it('exposes nextTab method', async () => {
100
+ const wrapper = mount(Tabs, {
101
+ props: {
102
+ tabs: mockTabs,
103
+ activeTab: 'tab1'
104
+ }
105
+ });
106
+
107
+ const component = wrapper.vm as any;
108
+ component.nextTab();
109
+
110
+ await wrapper.vm.$nextTick();
111
+ expect(wrapper.emitted('update:activeTab')).toBeTruthy();
112
+ expect(wrapper.emitted('update:activeTab')?.[0]).toEqual(['tab2']);
113
+ });
114
+
115
+ it('exposes previousTab method', async () => {
116
+ const wrapper = mount(Tabs, {
117
+ props: {
118
+ tabs: mockTabs,
119
+ activeTab: 'tab2'
120
+ }
121
+ });
122
+
123
+ const component = wrapper.vm as any;
124
+ component.previousTab();
125
+
126
+ await wrapper.vm.$nextTick();
127
+ expect(wrapper.emitted('update:activeTab')).toBeTruthy();
128
+ expect(wrapper.emitted('update:activeTab')?.[0]).toEqual(['tab1']);
129
+ });
130
+
131
+ it('does not go beyond last tab with nextTab', async () => {
132
+ const wrapper = mount(Tabs, {
133
+ props: {
134
+ tabs: mockTabs,
135
+ activeTab: 'tab3'
136
+ }
137
+ });
138
+
139
+ const component = wrapper.vm as any;
140
+ component.nextTab();
141
+
142
+ await wrapper.vm.$nextTick();
143
+ // Should not emit since we're already at the last tab
144
+ expect(component.getActiveTab()).toBe('tab3');
145
+ });
146
+
147
+ it('does not go before first tab with previousTab', async () => {
148
+ const wrapper = mount(Tabs, {
149
+ props: {
150
+ tabs: mockTabs,
151
+ activeTab: 'tab1'
152
+ }
153
+ });
154
+
155
+ const component = wrapper.vm as any;
156
+ component.previousTab();
157
+
158
+ await wrapper.vm.$nextTick();
159
+ // Should not emit since we're already at the first tab
160
+ expect(component.getActiveTab()).toBe('tab1');
161
+ });
162
+
163
+ it('renders tab content slot', () => {
164
+ const wrapper = mount(Tabs, {
165
+ props: {
166
+ tabs: mockTabs,
167
+ activeTab: 'tab1'
168
+ },
169
+ slots: {
170
+ tab1: '<div class="tab1-content">Tab 1 Content</div>'
171
+ }
172
+ });
173
+
174
+ expect(wrapper.find('.tabs-content').exists()).toBe(true);
175
+ });
176
+ });