@jmruthers/pace-core 0.5.140 → 0.5.141

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 (156) hide show
  1. package/README.md +2 -2
  2. package/dist/{DataTable-JXFCA2BJ.js → DataTable-EGIN2NKK.js} +3 -3
  3. package/dist/{EventLogo-rFL_kRjk.d.ts → EventLogo-B3V3otev.d.ts} +307 -1
  4. package/dist/{chunk-BOOI7GK2.js → chunk-3R472UXR.js} +117 -1
  5. package/dist/chunk-3R472UXR.js.map +1 -0
  6. package/dist/{chunk-5JMOHWDI.js → chunk-ALUN6O3G.js} +492 -324
  7. package/dist/chunk-ALUN6O3G.js.map +1 -0
  8. package/dist/{chunk-6DXZ6V5Q.js → chunk-PZV3XZKJ.js} +2 -2
  9. package/dist/{chunk-TLT2ZR3L.js → chunk-WKTQM2IC.js} +2 -2
  10. package/dist/components.d.ts +3 -1
  11. package/dist/components.js +15 -3
  12. package/dist/components.js.map +1 -1
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +18 -4
  15. package/dist/index.js.map +1 -1
  16. package/dist/rbac/index.d.ts +94 -1
  17. package/dist/rbac/index.js +4 -2
  18. package/dist/utils.js +1 -1
  19. package/docs/api/README.md +2 -2
  20. package/docs/api/classes/ColumnFactory.md +1 -1
  21. package/docs/api/classes/ErrorBoundary.md +1 -1
  22. package/docs/api/classes/InvalidScopeError.md +1 -1
  23. package/docs/api/classes/MissingUserContextError.md +1 -1
  24. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  25. package/docs/api/classes/PermissionDeniedError.md +1 -1
  26. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  27. package/docs/api/classes/RBACAuditManager.md +1 -1
  28. package/docs/api/classes/RBACCache.md +1 -1
  29. package/docs/api/classes/RBACEngine.md +1 -1
  30. package/docs/api/classes/RBACError.md +1 -1
  31. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  32. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  33. package/docs/api/classes/StorageUtils.md +1 -1
  34. package/docs/api/enums/FileCategory.md +1 -1
  35. package/docs/api/interfaces/AggregateConfig.md +1 -1
  36. package/docs/api/interfaces/BadgeProps.md +1 -1
  37. package/docs/api/interfaces/ButtonProps.md +1 -1
  38. package/docs/api/interfaces/CalendarProps.md +40 -0
  39. package/docs/api/interfaces/CardProps.md +1 -1
  40. package/docs/api/interfaces/ColorPalette.md +1 -1
  41. package/docs/api/interfaces/ColorShade.md +1 -1
  42. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  43. package/docs/api/interfaces/DataRecord.md +1 -1
  44. package/docs/api/interfaces/DataTableAction.md +1 -1
  45. package/docs/api/interfaces/DataTableColumn.md +1 -1
  46. package/docs/api/interfaces/DataTableProps.md +1 -1
  47. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  48. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  49. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  50. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  51. package/docs/api/interfaces/EventLogoProps.md +1 -1
  52. package/docs/api/interfaces/ExportColumn.md +1 -1
  53. package/docs/api/interfaces/ExportOptions.md +1 -1
  54. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  55. package/docs/api/interfaces/FileMetadata.md +1 -1
  56. package/docs/api/interfaces/FileReference.md +1 -1
  57. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  58. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  59. package/docs/api/interfaces/FileUploadProps.md +1 -1
  60. package/docs/api/interfaces/FooterProps.md +1 -1
  61. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  62. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  63. package/docs/api/interfaces/InputProps.md +1 -1
  64. package/docs/api/interfaces/LabelProps.md +1 -1
  65. package/docs/api/interfaces/LoginFormProps.md +1 -1
  66. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  67. package/docs/api/interfaces/NavigationContextType.md +1 -1
  68. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  69. package/docs/api/interfaces/NavigationItem.md +1 -1
  70. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  71. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  72. package/docs/api/interfaces/Organisation.md +1 -1
  73. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  74. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  75. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  76. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  77. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  78. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  79. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  80. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  81. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  82. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  83. package/docs/api/interfaces/PaletteData.md +1 -1
  84. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  85. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  86. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  87. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  88. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  89. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  90. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  91. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  92. package/docs/api/interfaces/RBACConfig.md +1 -1
  93. package/docs/api/interfaces/RBACLogger.md +1 -1
  94. package/docs/api/interfaces/ResourcePermissions.md +155 -0
  95. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  96. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  97. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  98. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  99. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  100. package/docs/api/interfaces/RouteConfig.md +1 -1
  101. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  102. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  103. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  104. package/docs/api/interfaces/StorageConfig.md +1 -1
  105. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  106. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  107. package/docs/api/interfaces/StorageListOptions.md +1 -1
  108. package/docs/api/interfaces/StorageListResult.md +1 -1
  109. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  110. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  111. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  112. package/docs/api/interfaces/StyleImport.md +1 -1
  113. package/docs/api/interfaces/SwitchProps.md +1 -1
  114. package/docs/api/interfaces/TabsContentProps.md +9 -0
  115. package/docs/api/interfaces/TabsListProps.md +9 -0
  116. package/docs/api/interfaces/TabsProps.md +9 -0
  117. package/docs/api/interfaces/TabsTriggerProps.md +9 -0
  118. package/docs/api/interfaces/TextareaProps.md +53 -0
  119. package/docs/api/interfaces/ToastActionElement.md +1 -1
  120. package/docs/api/interfaces/ToastProps.md +1 -1
  121. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  122. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  123. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  124. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  125. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  126. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  127. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  128. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  129. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  130. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  131. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  132. package/docs/api/interfaces/UseResourcePermissionsOptions.md +34 -0
  133. package/docs/api/interfaces/UserEventAccess.md +1 -1
  134. package/docs/api/interfaces/UserMenuProps.md +1 -1
  135. package/docs/api/interfaces/UserProfile.md +1 -1
  136. package/docs/api/modules.md +289 -2
  137. package/package.json +3 -1
  138. package/src/components/Calendar/Calendar.test.tsx +338 -0
  139. package/src/components/Calendar/Calendar.tsx +192 -0
  140. package/src/components/Calendar/index.ts +10 -0
  141. package/src/components/Tabs/Tabs.test.tsx +439 -0
  142. package/src/components/Tabs/Tabs.tsx +202 -0
  143. package/src/components/Tabs/index.ts +10 -0
  144. package/src/components/Textarea/Textarea.test.tsx +269 -0
  145. package/src/components/Textarea/Textarea.tsx +133 -0
  146. package/src/components/Textarea/index.ts +10 -0
  147. package/src/components/index.ts +11 -0
  148. package/src/index.ts +11 -0
  149. package/src/rbac/hooks/index.ts +2 -0
  150. package/src/rbac/hooks/useResourcePermissions.test.ts +633 -0
  151. package/src/rbac/hooks/useResourcePermissions.ts +235 -0
  152. package/dist/chunk-5JMOHWDI.js.map +0 -1
  153. package/dist/chunk-BOOI7GK2.js.map +0 -1
  154. /package/dist/{DataTable-JXFCA2BJ.js.map → DataTable-EGIN2NKK.js.map} +0 -0
  155. /package/dist/{chunk-6DXZ6V5Q.js.map → chunk-PZV3XZKJ.js.map} +0 -0
  156. /package/dist/{chunk-TLT2ZR3L.js.map → chunk-WKTQM2IC.js.map} +0 -0
@@ -0,0 +1,439 @@
1
+ /**
2
+ * @file Tabs Component Tests
3
+ * @description Comprehensive tests for Tabs component system
4
+ * @package @jmruthers/pace-core
5
+ */
6
+
7
+ import React from 'react';
8
+ import { screen } from '@testing-library/react';
9
+ import userEvent from '@testing-library/user-event';
10
+ import { describe, it, expect, vi } from 'vitest';
11
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs';
12
+ import { renderWithProviders } from '../../__tests__/helpers/test-utils';
13
+
14
+ describe('Tabs Component', () => {
15
+ // Basic rendering tests
16
+ describe('Rendering', () => {
17
+ it('renders tabs with default value', () => {
18
+ renderWithProviders(
19
+ <Tabs defaultValue="tab1">
20
+ <TabsList>
21
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
22
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
23
+ </TabsList>
24
+ <TabsContent value="tab1">Content 1</TabsContent>
25
+ <TabsContent value="tab2">Content 2</TabsContent>
26
+ </Tabs>
27
+ );
28
+
29
+ expect(screen.getByText('Tab 1')).toBeInTheDocument();
30
+ expect(screen.getByText('Tab 2')).toBeInTheDocument();
31
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
32
+ });
33
+
34
+ it('renders with custom className', () => {
35
+ renderWithProviders(
36
+ <Tabs defaultValue="tab1" className="custom-tabs">
37
+ <TabsList>
38
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
39
+ </TabsList>
40
+ <TabsContent value="tab1">Content 1</TabsContent>
41
+ </Tabs>
42
+ );
43
+
44
+ const tabsRoot = screen.getByText('Tab 1').closest('[role="tablist"]')?.parentElement;
45
+ expect(tabsRoot).toBeInTheDocument();
46
+ });
47
+
48
+ it('renders TabsList with custom className', () => {
49
+ renderWithProviders(
50
+ <Tabs defaultValue="tab1">
51
+ <TabsList className="custom-list">
52
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
53
+ </TabsList>
54
+ <TabsContent value="tab1">Content 1</TabsContent>
55
+ </Tabs>
56
+ );
57
+
58
+ const tabsList = screen.getByRole('tablist');
59
+ expect(tabsList).toHaveClass('custom-list');
60
+ });
61
+
62
+ it('renders TabsTrigger with custom className', () => {
63
+ renderWithProviders(
64
+ <Tabs defaultValue="tab1">
65
+ <TabsList>
66
+ <TabsTrigger value="tab1" className="custom-trigger">Tab 1</TabsTrigger>
67
+ </TabsList>
68
+ <TabsContent value="tab1">Content 1</TabsContent>
69
+ </Tabs>
70
+ );
71
+
72
+ const trigger = screen.getByRole('tab', { name: 'Tab 1' });
73
+ expect(trigger).toHaveClass('custom-trigger');
74
+ });
75
+
76
+ it('renders TabsContent with custom className', () => {
77
+ renderWithProviders(
78
+ <Tabs defaultValue="tab1">
79
+ <TabsList>
80
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
81
+ </TabsList>
82
+ <TabsContent value="tab1" className="custom-content">Content 1</TabsContent>
83
+ </Tabs>
84
+ );
85
+
86
+ const content = screen.getByText('Content 1');
87
+ expect(content).toHaveClass('custom-content');
88
+ });
89
+ });
90
+
91
+ // Controlled mode tests
92
+ describe('Controlled Mode', () => {
93
+ it('handles controlled state', () => {
94
+ const TestComponent = () => {
95
+ const [value, setValue] = React.useState('tab1');
96
+ return (
97
+ <Tabs value={value} onValueChange={setValue}>
98
+ <TabsList>
99
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
100
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
101
+ </TabsList>
102
+ <TabsContent value="tab1">Content 1</TabsContent>
103
+ <TabsContent value="tab2">Content 2</TabsContent>
104
+ </Tabs>
105
+ );
106
+ };
107
+
108
+ renderWithProviders(<TestComponent />);
109
+
110
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
111
+ expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
112
+ });
113
+
114
+ it('calls onValueChange when tab is clicked', async () => {
115
+ const handleValueChange = vi.fn();
116
+ const user = userEvent.setup();
117
+
118
+ renderWithProviders(
119
+ <Tabs value="tab1" onValueChange={handleValueChange}>
120
+ <TabsList>
121
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
122
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
123
+ </TabsList>
124
+ <TabsContent value="tab1">Content 1</TabsContent>
125
+ <TabsContent value="tab2">Content 2</TabsContent>
126
+ </Tabs>
127
+ );
128
+
129
+ const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
130
+ await user.click(tab2);
131
+
132
+ expect(handleValueChange).toHaveBeenCalledWith('tab2');
133
+ });
134
+ });
135
+
136
+ // Uncontrolled mode tests
137
+ describe('Uncontrolled Mode', () => {
138
+ it('handles uncontrolled state with defaultValue', async () => {
139
+ const user = userEvent.setup();
140
+
141
+ renderWithProviders(
142
+ <Tabs defaultValue="tab1">
143
+ <TabsList>
144
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
145
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
146
+ </TabsList>
147
+ <TabsContent value="tab1">Content 1</TabsContent>
148
+ <TabsContent value="tab2">Content 2</TabsContent>
149
+ </Tabs>
150
+ );
151
+
152
+ expect(screen.getByText('Content 1')).toBeVisible();
153
+
154
+ const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
155
+ await user.click(tab2);
156
+
157
+ expect(screen.getByText('Content 2')).toBeVisible();
158
+ expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
159
+ });
160
+ });
161
+
162
+ // Keyboard navigation tests
163
+ describe('Keyboard Navigation', () => {
164
+ it('navigates with ArrowRight key', async () => {
165
+ const user = userEvent.setup();
166
+
167
+ renderWithProviders(
168
+ <Tabs defaultValue="tab1">
169
+ <TabsList>
170
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
171
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
172
+ <TabsTrigger value="tab3">Tab 3</TabsTrigger>
173
+ </TabsList>
174
+ <TabsContent value="tab1">Content 1</TabsContent>
175
+ <TabsContent value="tab2">Content 2</TabsContent>
176
+ <TabsContent value="tab3">Content 3</TabsContent>
177
+ </Tabs>
178
+ );
179
+
180
+ const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
181
+ tab1.focus();
182
+
183
+ await user.keyboard('{ArrowRight}');
184
+
185
+ const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
186
+ expect(tab2).toHaveFocus();
187
+ });
188
+
189
+ it('navigates with ArrowLeft key', async () => {
190
+ const user = userEvent.setup();
191
+
192
+ renderWithProviders(
193
+ <Tabs defaultValue="tab2">
194
+ <TabsList>
195
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
196
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
197
+ <TabsTrigger value="tab3">Tab 3</TabsTrigger>
198
+ </TabsList>
199
+ <TabsContent value="tab1">Content 1</TabsContent>
200
+ <TabsContent value="tab2">Content 2</TabsContent>
201
+ <TabsContent value="tab3">Content 3</TabsContent>
202
+ </Tabs>
203
+ );
204
+
205
+ const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
206
+ tab2.focus();
207
+
208
+ await user.keyboard('{ArrowLeft}');
209
+
210
+ const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
211
+ expect(tab1).toHaveFocus();
212
+ });
213
+
214
+ it('navigates to first tab with Home key', async () => {
215
+ const user = userEvent.setup();
216
+
217
+ renderWithProviders(
218
+ <Tabs defaultValue="tab3">
219
+ <TabsList>
220
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
221
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
222
+ <TabsTrigger value="tab3">Tab 3</TabsTrigger>
223
+ </TabsList>
224
+ <TabsContent value="tab1">Content 1</TabsContent>
225
+ <TabsContent value="tab2">Content 2</TabsContent>
226
+ <TabsContent value="tab3">Content 3</TabsContent>
227
+ </Tabs>
228
+ );
229
+
230
+ const tab3 = screen.getByRole('tab', { name: 'Tab 3' });
231
+ tab3.focus();
232
+
233
+ await user.keyboard('{Home}');
234
+
235
+ const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
236
+ expect(tab1).toHaveFocus();
237
+ });
238
+
239
+ it('navigates to last tab with End key', async () => {
240
+ const user = userEvent.setup();
241
+
242
+ renderWithProviders(
243
+ <Tabs defaultValue="tab1">
244
+ <TabsList>
245
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
246
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
247
+ <TabsTrigger value="tab3">Tab 3</TabsTrigger>
248
+ </TabsList>
249
+ <TabsContent value="tab1">Content 1</TabsContent>
250
+ <TabsContent value="tab2">Content 2</TabsContent>
251
+ <TabsContent value="tab3">Content 3</TabsContent>
252
+ </Tabs>
253
+ );
254
+
255
+ const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
256
+ tab1.focus();
257
+
258
+ await user.keyboard('{End}');
259
+
260
+ const tab3 = screen.getByRole('tab', { name: 'Tab 3' });
261
+ expect(tab3).toHaveFocus();
262
+ });
263
+ });
264
+
265
+ // Accessibility tests
266
+ describe('Accessibility', () => {
267
+ it('has proper ARIA attributes', () => {
268
+ renderWithProviders(
269
+ <Tabs defaultValue="tab1">
270
+ <TabsList>
271
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
272
+ </TabsList>
273
+ <TabsContent value="tab1">Content 1</TabsContent>
274
+ </Tabs>
275
+ );
276
+
277
+ const tabsList = screen.getByRole('tablist');
278
+ expect(tabsList).toBeInTheDocument();
279
+
280
+ const tab = screen.getByRole('tab', { name: 'Tab 1' });
281
+ expect(tab).toHaveAttribute('aria-selected');
282
+
283
+ const tabPanel = screen.getByRole('tabpanel');
284
+ expect(tabPanel).toBeInTheDocument();
285
+ });
286
+
287
+ it('associates tab panels with triggers', () => {
288
+ renderWithProviders(
289
+ <Tabs defaultValue="tab1">
290
+ <TabsList>
291
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
292
+ </TabsList>
293
+ <TabsContent value="tab1">Content 1</TabsContent>
294
+ </Tabs>
295
+ );
296
+
297
+ const tab = screen.getByRole('tab', { name: 'Tab 1' });
298
+ const tabPanel = screen.getByRole('tabpanel');
299
+
300
+ const tabId = tab.getAttribute('id');
301
+ const panelLabelledBy = tabPanel.getAttribute('aria-labelledby');
302
+
303
+ expect(panelLabelledBy).toBe(tabId);
304
+ });
305
+
306
+ it('marks active tab with aria-selected="true"', () => {
307
+ renderWithProviders(
308
+ <Tabs defaultValue="tab1">
309
+ <TabsList>
310
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
311
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
312
+ </TabsList>
313
+ <TabsContent value="tab1">Content 1</TabsContent>
314
+ <TabsContent value="tab2">Content 2</TabsContent>
315
+ </Tabs>
316
+ );
317
+
318
+ const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
319
+ const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
320
+
321
+ expect(tab1).toHaveAttribute('aria-selected', 'true');
322
+ expect(tab2).toHaveAttribute('aria-selected', 'false');
323
+ });
324
+
325
+ it('has focus visible styles', () => {
326
+ renderWithProviders(
327
+ <Tabs defaultValue="tab1">
328
+ <TabsList>
329
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
330
+ </TabsList>
331
+ <TabsContent value="tab1">Content 1</TabsContent>
332
+ </Tabs>
333
+ );
334
+
335
+ const tab = screen.getByRole('tab', { name: 'Tab 1' });
336
+ expect(tab).toHaveClass('focus-visible:outline-none', 'focus-visible:ring-2');
337
+ });
338
+ });
339
+
340
+ // Visual states tests
341
+ describe('Visual States', () => {
342
+ it('applies active state styling', () => {
343
+ renderWithProviders(
344
+ <Tabs defaultValue="tab1">
345
+ <TabsList>
346
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
347
+ </TabsList>
348
+ <TabsContent value="tab1">Content 1</TabsContent>
349
+ </Tabs>
350
+ );
351
+
352
+ const tab = screen.getByRole('tab', { name: 'Tab 1' });
353
+ expect(tab).toHaveClass('data-[state=active]:bg-background', 'data-[state=active]:text-main-600');
354
+ });
355
+
356
+ it('applies inactive state styling', () => {
357
+ renderWithProviders(
358
+ <Tabs defaultValue="tab1">
359
+ <TabsList>
360
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
361
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
362
+ </TabsList>
363
+ <TabsContent value="tab1">Content 1</TabsContent>
364
+ <TabsContent value="tab2">Content 2</TabsContent>
365
+ </Tabs>
366
+ );
367
+
368
+ const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
369
+ expect(tab2).toHaveClass('data-[state=inactive]:text-sec-600');
370
+ });
371
+ });
372
+
373
+ // Integration tests
374
+ describe('Integration', () => {
375
+ it('works with icons in triggers', () => {
376
+ const TestIcon = () => <span data-testid="icon">Icon</span>;
377
+
378
+ renderWithProviders(
379
+ <Tabs defaultValue="tab1">
380
+ <TabsList>
381
+ <TabsTrigger value="tab1">
382
+ <TestIcon /> Tab 1
383
+ </TabsTrigger>
384
+ </TabsList>
385
+ <TabsContent value="tab1">Content 1</TabsContent>
386
+ </Tabs>
387
+ );
388
+
389
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
390
+ expect(screen.getByText('Tab 1')).toBeInTheDocument();
391
+ });
392
+
393
+ it('forwards refs correctly', () => {
394
+ const tabsRef = React.createRef<HTMLDivElement>();
395
+ const listRef = React.createRef<HTMLDivElement>();
396
+ const triggerRef = React.createRef<HTMLButtonElement>();
397
+ const contentRef = React.createRef<HTMLDivElement>();
398
+
399
+ renderWithProviders(
400
+ <Tabs ref={tabsRef} defaultValue="tab1">
401
+ <TabsList ref={listRef}>
402
+ <TabsTrigger ref={triggerRef} value="tab1">Tab 1</TabsTrigger>
403
+ </TabsList>
404
+ <TabsContent ref={contentRef} value="tab1">Content 1</TabsContent>
405
+ </Tabs>
406
+ );
407
+
408
+ expect(tabsRef.current).toBeInstanceOf(HTMLDivElement);
409
+ expect(listRef.current).toBeInstanceOf(HTMLDivElement);
410
+ expect(triggerRef.current).toBeInstanceOf(HTMLButtonElement);
411
+ expect(contentRef.current).toBeInstanceOf(HTMLDivElement);
412
+ });
413
+
414
+ it('works with multiple tab groups', () => {
415
+ renderWithProviders(
416
+ <div>
417
+ <Tabs defaultValue="tab1">
418
+ <TabsList>
419
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
420
+ </TabsList>
421
+ <TabsContent value="tab1">Content 1</TabsContent>
422
+ </Tabs>
423
+ <Tabs defaultValue="tabA">
424
+ <TabsList>
425
+ <TabsTrigger value="tabA">Tab A</TabsTrigger>
426
+ </TabsList>
427
+ <TabsContent value="tabA">Content A</TabsContent>
428
+ </Tabs>
429
+ </div>
430
+ );
431
+
432
+ expect(screen.getByText('Tab 1')).toBeInTheDocument();
433
+ expect(screen.getByText('Tab A')).toBeInTheDocument();
434
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
435
+ expect(screen.getByText('Content A')).toBeInTheDocument();
436
+ });
437
+ });
438
+ });
439
+
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @file Tabs Component System
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/Tabs
5
+ * @since 0.5.141
6
+ *
7
+ * A comprehensive tabs component system built on top of Radix UI primitives.
8
+ * Provides accessible tabbed interfaces with keyboard navigation and ARIA support.
9
+ *
10
+ * Features:
11
+ * - Controlled and uncontrolled modes
12
+ * - Keyboard navigation (arrow keys)
13
+ * - Accessible ARIA attributes
14
+ * - Customizable styling via className
15
+ * - Support for icons in triggers
16
+ * - Compound component pattern
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * // Basic tabs
21
+ * <Tabs defaultValue="tab1">
22
+ * <TabsList>
23
+ * <TabsTrigger value="tab1">Tab 1</TabsTrigger>
24
+ * <TabsTrigger value="tab2">Tab 2</TabsTrigger>
25
+ * </TabsList>
26
+ * <TabsContent value="tab1">Content 1</TabsContent>
27
+ * <TabsContent value="tab2">Content 2</TabsContent>
28
+ * </Tabs>
29
+ *
30
+ * // Controlled tabs
31
+ * <Tabs value={activeTab} onValueChange={setActiveTab}>
32
+ * <TabsList>
33
+ * <TabsTrigger value="transport">
34
+ * <Plane size={16} /> Transport
35
+ * </TabsTrigger>
36
+ * <TabsTrigger value="accommodation">
37
+ * <Building size={16} /> Accommodation
38
+ * </TabsTrigger>
39
+ * </TabsList>
40
+ * <TabsContent value="transport">
41
+ * <TransportPlanningView />
42
+ * </TabsContent>
43
+ * <TabsContent value="accommodation">
44
+ * <AccommodationPlanningView />
45
+ * </TabsContent>
46
+ * </Tabs>
47
+ * ```
48
+ *
49
+ * @accessibility
50
+ * - WCAG 2.1 AA compliant
51
+ * - Keyboard navigation (Arrow keys, Home, End)
52
+ * - Screen reader support with proper ARIA attributes
53
+ * - Focus management
54
+ * - Tab panel association
55
+ */
56
+
57
+ import * as React from 'react';
58
+ import * as TabsPrimitive from '@radix-ui/react-tabs';
59
+ import { cn } from '../../utils/core/cn';
60
+
61
+ // ============================================================================
62
+ // TABS ROOT COMPONENT
63
+ // ============================================================================
64
+
65
+ export interface TabsProps extends TabsPrimitive.TabsProps {}
66
+
67
+ /**
68
+ * Tabs root component
69
+ * Provides the context for tab navigation and state management
70
+ *
71
+ * @component
72
+ * @example
73
+ * ```tsx
74
+ * <Tabs defaultValue="tab1">
75
+ * <TabsList>...</TabsList>
76
+ * <TabsContent value="tab1">...</TabsContent>
77
+ * </Tabs>
78
+ * ```
79
+ */
80
+ const Tabs = React.forwardRef<
81
+ React.ElementRef<typeof TabsPrimitive.Root>,
82
+ TabsProps
83
+ >(({ className, ...props }, ref) => (
84
+ <TabsPrimitive.Root ref={ref} {...props} />
85
+ ));
86
+
87
+ Tabs.displayName = TabsPrimitive.Root.displayName || 'Tabs';
88
+
89
+ // ============================================================================
90
+ // TABS LIST COMPONENT
91
+ // ============================================================================
92
+
93
+ export interface TabsListProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> {}
94
+
95
+ /**
96
+ * TabsList component
97
+ * Container for tab triggers
98
+ *
99
+ * @component
100
+ * @example
101
+ * ```tsx
102
+ * <TabsList>
103
+ * <TabsTrigger value="tab1">Tab 1</TabsTrigger>
104
+ * <TabsTrigger value="tab2">Tab 2</TabsTrigger>
105
+ * </TabsList>
106
+ * ```
107
+ */
108
+ const TabsList = React.forwardRef<
109
+ React.ElementRef<typeof TabsPrimitive.List>,
110
+ TabsListProps
111
+ >(({ className, ...props }, ref) => (
112
+ <TabsPrimitive.List
113
+ ref={ref}
114
+ className={cn(
115
+ 'inline-flex h-10 items-center justify-center rounded-md bg-sec-100 p-1 text-sec-600',
116
+ className
117
+ )}
118
+ {...props}
119
+ />
120
+ ));
121
+
122
+ TabsList.displayName = TabsPrimitive.List.displayName || 'TabsList';
123
+
124
+ // ============================================================================
125
+ // TABS TRIGGER COMPONENT
126
+ // ============================================================================
127
+
128
+ export interface TabsTriggerProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> {}
129
+
130
+ /**
131
+ * TabsTrigger component
132
+ * Individual tab button that activates a tab panel
133
+ *
134
+ * @component
135
+ * @example
136
+ * ```tsx
137
+ * <TabsTrigger value="tab1">Tab 1</TabsTrigger>
138
+ *
139
+ * // With icon
140
+ * <TabsTrigger value="transport">
141
+ * <Plane size={16} /> Transport
142
+ * </TabsTrigger>
143
+ * ```
144
+ */
145
+ const TabsTrigger = React.forwardRef<
146
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
147
+ TabsTriggerProps
148
+ >(({ className, ...props }, ref) => (
149
+ <TabsPrimitive.Trigger
150
+ ref={ref}
151
+ className={cn(
152
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
153
+ 'data-[state=active]:bg-background data-[state=active]:text-main-600 data-[state=active]:shadow-sm',
154
+ 'data-[state=inactive]:text-sec-600 data-[state=inactive]:hover:text-main-600',
155
+ className
156
+ )}
157
+ {...props}
158
+ />
159
+ ));
160
+
161
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName || 'TabsTrigger';
162
+
163
+ // ============================================================================
164
+ // TABS CONTENT COMPONENT
165
+ // ============================================================================
166
+
167
+ export interface TabsContentProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> {}
168
+
169
+ /**
170
+ * TabsContent component
171
+ * Container for tab panel content
172
+ *
173
+ * @component
174
+ * @example
175
+ * ```tsx
176
+ * <TabsContent value="tab1">
177
+ * <div>Content for tab 1</div>
178
+ * </TabsContent>
179
+ * ```
180
+ */
181
+ const TabsContent = React.forwardRef<
182
+ React.ElementRef<typeof TabsPrimitive.Content>,
183
+ TabsContentProps
184
+ >(({ className, ...props }, ref) => (
185
+ <TabsPrimitive.Content
186
+ ref={ref}
187
+ className={cn(
188
+ 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2',
189
+ className
190
+ )}
191
+ {...props}
192
+ />
193
+ ));
194
+
195
+ TabsContent.displayName = TabsPrimitive.Content.displayName || 'TabsContent';
196
+
197
+ // ============================================================================
198
+ // EXPORTS
199
+ // ============================================================================
200
+
201
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
202
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file Tabs component exports
3
+ * @package @jmruthers/pace-core
4
+ * @module Tabs
5
+ * @since 0.5.141
6
+ */
7
+
8
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs';
9
+ export type { TabsProps, TabsListProps, TabsTriggerProps, TabsContentProps } from './Tabs';
10
+