@jmruthers/pace-core 0.5.139 → 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.
- package/README.md +2 -2
- package/dist/{DataTable-JXFCA2BJ.js → DataTable-EGIN2NKK.js} +3 -3
- package/dist/{EventLogo-rFL_kRjk.d.ts → EventLogo-B3V3otev.d.ts} +307 -1
- package/dist/{chunk-BOOI7GK2.js → chunk-3R472UXR.js} +117 -1
- package/dist/chunk-3R472UXR.js.map +1 -0
- package/dist/{chunk-5JMOHWDI.js → chunk-ALUN6O3G.js} +492 -324
- package/dist/chunk-ALUN6O3G.js.map +1 -0
- package/dist/{chunk-6DXZ6V5Q.js → chunk-PZV3XZKJ.js} +2 -2
- package/dist/{chunk-TLT2ZR3L.js → chunk-WKTQM2IC.js} +2 -2
- package/dist/components.d.ts +3 -1
- package/dist/components.js +15 -3
- package/dist/components.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/rbac/index.d.ts +94 -1
- package/dist/rbac/index.js +4 -2
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +17 -5
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +2 -2
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +40 -0
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +155 -0
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +9 -0
- package/docs/api/interfaces/TabsListProps.md +9 -0
- package/docs/api/interfaces/TabsProps.md +9 -0
- package/docs/api/interfaces/TabsTriggerProps.md +9 -0
- package/docs/api/interfaces/TextareaProps.md +53 -0
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +34 -0
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +289 -2
- package/docs/getting-started/examples/basic-auth-app.md +196 -0
- package/docs/getting-started/examples/full-featured-app.md +616 -0
- package/package.json +3 -1
- package/src/components/Calendar/Calendar.test.tsx +338 -0
- package/src/components/Calendar/Calendar.tsx +192 -0
- package/src/components/Calendar/index.ts +10 -0
- package/src/components/Tabs/Tabs.test.tsx +439 -0
- package/src/components/Tabs/Tabs.tsx +202 -0
- package/src/components/Tabs/index.ts +10 -0
- package/src/components/Textarea/Textarea.test.tsx +269 -0
- package/src/components/Textarea/Textarea.tsx +133 -0
- package/src/components/Textarea/index.ts +10 -0
- package/src/components/index.ts +11 -0
- package/src/index.ts +11 -0
- package/src/rbac/hooks/index.ts +2 -0
- package/src/rbac/hooks/useResourcePermissions.test.ts +633 -0
- package/src/rbac/hooks/useResourcePermissions.ts +235 -0
- package/src/utils/performance/bundleAnalysis.ts +17 -3
- package/dist/chunk-5JMOHWDI.js.map +0 -1
- package/dist/chunk-BOOI7GK2.js.map +0 -1
- /package/dist/{DataTable-JXFCA2BJ.js.map → DataTable-EGIN2NKK.js.map} +0 -0
- /package/dist/{chunk-6DXZ6V5Q.js.map → chunk-PZV3XZKJ.js.map} +0 -0
- /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
|
+
|