@jmruthers/pace-core 0.5.140 → 0.5.142
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-SKCX4SCB.js} +6 -6
- package/dist/{EventLogo-rFL_kRjk.d.ts → EventLogo-B3V3otev.d.ts} +307 -1
- package/dist/{UnifiedAuthProvider-XIQQ7LVU.js → UnifiedAuthProvider-BMJAP6Z7.js} +3 -3
- package/dist/{chunk-22WKWKRX.js → chunk-2AKRP5QZ.js} +4 -4
- package/dist/{chunk-4C7EXCAR.js → chunk-CRGFNQ2L.js} +4 -4
- package/dist/{chunk-TLT2ZR3L.js → chunk-E6ZCVF4T.js} +4 -4
- package/dist/{chunk-INQLMHPF.js → chunk-ERGKJX4D.js} +2 -2
- package/dist/{chunk-6LAAY47Q.js → chunk-MSHEVJXS.js} +2 -2
- package/dist/{chunk-MA6EPSGZ.js → chunk-PKW27QVS.js} +2 -2
- package/dist/{chunk-T6JN6LH6.js → chunk-R53TUSFK.js} +3 -3
- package/dist/{chunk-6DXZ6V5Q.js → chunk-SFVL7ZFI.js} +5 -5
- package/dist/{chunk-5JMOHWDI.js → chunk-TUJSIWX6.js} +497 -329
- package/dist/chunk-TUJSIWX6.js.map +1 -0
- package/dist/{chunk-BOOI7GK2.js → chunk-VOJBGZYI.js} +119 -3
- package/dist/chunk-VOJBGZYI.js.map +1 -0
- package/dist/{chunk-YCWDTTUK.js → chunk-WM26XK7I.js} +22 -8
- package/dist/chunk-WM26XK7I.js.map +1 -0
- package/dist/components.d.ts +3 -1
- package/dist/components.js +20 -8
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.js +25 -11
- package/dist/index.js.map +1 -1
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +94 -1
- package/dist/rbac/index.js +9 -7
- package/dist/utils.js +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/rbac/README.md +2 -1
- package/docs/rbac/event-based-apps.md +872 -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/services/EventService.ts +29 -8
- package/src/services/__tests__/EventService.test.ts +48 -8
- package/dist/chunk-5JMOHWDI.js.map +0 -1
- package/dist/chunk-BOOI7GK2.js.map +0 -1
- package/dist/chunk-YCWDTTUK.js.map +0 -1
- package/src/rbac/docs/event-based-apps.md +0 -285
- /package/dist/{DataTable-JXFCA2BJ.js.map → DataTable-SKCX4SCB.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-XIQQ7LVU.js.map → UnifiedAuthProvider-BMJAP6Z7.js.map} +0 -0
- /package/dist/{chunk-22WKWKRX.js.map → chunk-2AKRP5QZ.js.map} +0 -0
- /package/dist/{chunk-4C7EXCAR.js.map → chunk-CRGFNQ2L.js.map} +0 -0
- /package/dist/{chunk-TLT2ZR3L.js.map → chunk-E6ZCVF4T.js.map} +0 -0
- /package/dist/{chunk-INQLMHPF.js.map → chunk-ERGKJX4D.js.map} +0 -0
- /package/dist/{chunk-6LAAY47Q.js.map → chunk-MSHEVJXS.js.map} +0 -0
- /package/dist/{chunk-MA6EPSGZ.js.map → chunk-PKW27QVS.js.map} +0 -0
- /package/dist/{chunk-T6JN6LH6.js.map → chunk-R53TUSFK.js.map} +0 -0
- /package/dist/{chunk-6DXZ6V5Q.js.map → chunk-SFVL7ZFI.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmruthers/pace-core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.142",
|
|
4
4
|
"description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -199,6 +199,7 @@
|
|
|
199
199
|
"@radix-ui/react-progress": "^1.0.0",
|
|
200
200
|
"@radix-ui/react-slot": "^1.0.0",
|
|
201
201
|
"@radix-ui/react-switch": "^1.1.0",
|
|
202
|
+
"@radix-ui/react-tabs": "^1.0.0",
|
|
202
203
|
"@radix-ui/react-toast": "^1.0.0",
|
|
203
204
|
"@radix-ui/react-tooltip": "^1.0.0",
|
|
204
205
|
"@tanstack/react-table": "^8.0.0",
|
|
@@ -206,6 +207,7 @@
|
|
|
206
207
|
"lucide-react": "^0.400.0",
|
|
207
208
|
"react": "^18.0.0",
|
|
208
209
|
"react-dom": "^18.0.0",
|
|
210
|
+
"react-day-picker": "^9.0.0",
|
|
209
211
|
"react-hook-form": "^7.0.0",
|
|
210
212
|
"react-router-dom": "^6.0.0",
|
|
211
213
|
"tailwind-merge": "^2.0.0",
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Calendar Component Tests
|
|
3
|
+
* @description Comprehensive tests for Calendar component
|
|
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, beforeEach } from 'vitest';
|
|
11
|
+
import { Calendar } from './Calendar';
|
|
12
|
+
import { renderWithProviders } from '../../__tests__/helpers/test-utils';
|
|
13
|
+
|
|
14
|
+
// Mock react-day-picker to avoid complex date rendering issues in tests
|
|
15
|
+
vi.mock('react-day-picker', () => {
|
|
16
|
+
const React = require('react');
|
|
17
|
+
return {
|
|
18
|
+
DayPicker: ({ selected, onSelect, disabled, mode, className, classNames, ...props }: any) => {
|
|
19
|
+
const handleDateClick = (date: Date) => {
|
|
20
|
+
if (disabled && typeof disabled === 'function' && disabled(date)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (onSelect) {
|
|
24
|
+
if (mode === 'multiple') {
|
|
25
|
+
const current = Array.isArray(selected) ? selected : [];
|
|
26
|
+
const dateStr = date.toISOString();
|
|
27
|
+
if (current.some((d: Date) => d.toISOString() === dateStr)) {
|
|
28
|
+
onSelect(current.filter((d: Date) => d.toISOString() !== dateStr));
|
|
29
|
+
} else {
|
|
30
|
+
onSelect([...current, date]);
|
|
31
|
+
}
|
|
32
|
+
} else if (mode === 'range') {
|
|
33
|
+
// Simplified range logic for testing
|
|
34
|
+
onSelect({ from: date, to: date });
|
|
35
|
+
} else {
|
|
36
|
+
onSelect(date);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const today = new Date();
|
|
42
|
+
const dates = Array.from({ length: 7 }, (_, i) => {
|
|
43
|
+
const date = new Date(today);
|
|
44
|
+
date.setDate(today.getDate() + i - 3);
|
|
45
|
+
return date;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div data-testid="calendar" className={className} {...props}>
|
|
50
|
+
<div className="calendar-header">Calendar</div>
|
|
51
|
+
<div className="calendar-grid">
|
|
52
|
+
{dates.map((date, idx) => {
|
|
53
|
+
const isDisabled = disabled && typeof disabled === 'function' && disabled(date);
|
|
54
|
+
const isSelected =
|
|
55
|
+
(mode === 'single' && selected && date.toDateString() === selected.toDateString()) ||
|
|
56
|
+
(mode === 'multiple' && Array.isArray(selected) && selected.some((d: Date) => d.toDateString() === date.toDateString())) ||
|
|
57
|
+
(mode === 'range' && selected?.from && date.toDateString() === selected.from.toDateString());
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<button
|
|
61
|
+
key={idx}
|
|
62
|
+
data-testid={`date-${date.toISOString().split('T')[0]}`}
|
|
63
|
+
onClick={() => !isDisabled && handleDateClick(date)}
|
|
64
|
+
disabled={isDisabled}
|
|
65
|
+
className={isSelected ? 'selected' : ''}
|
|
66
|
+
aria-selected={isSelected}
|
|
67
|
+
>
|
|
68
|
+
{date.getDate()}
|
|
69
|
+
</button>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('Calendar Component', () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
vi.clearAllMocks();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Basic rendering tests
|
|
85
|
+
describe('Rendering', () => {
|
|
86
|
+
it('renders calendar with default props', () => {
|
|
87
|
+
renderWithProviders(<Calendar />);
|
|
88
|
+
const calendar = screen.getByTestId('calendar');
|
|
89
|
+
expect(calendar).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('renders with custom className', () => {
|
|
93
|
+
renderWithProviders(<Calendar className="custom-calendar" />);
|
|
94
|
+
const calendar = screen.getByTestId('calendar');
|
|
95
|
+
// className is applied to the wrapper div, check parent element
|
|
96
|
+
const wrapper = calendar.parentElement;
|
|
97
|
+
expect(wrapper).toHaveClass('custom-calendar');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('forwards ref correctly', () => {
|
|
101
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
102
|
+
renderWithProviders(<Calendar ref={ref} />);
|
|
103
|
+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Single date selection tests
|
|
108
|
+
describe('Single Date Selection', () => {
|
|
109
|
+
it('handles single date selection', async () => {
|
|
110
|
+
const handleSelect = vi.fn();
|
|
111
|
+
const user = userEvent.setup();
|
|
112
|
+
|
|
113
|
+
renderWithProviders(
|
|
114
|
+
<Calendar mode="single" selected={undefined} onSelect={handleSelect} />
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const today = new Date();
|
|
118
|
+
const dateStr = today.toISOString().split('T')[0];
|
|
119
|
+
const dateButton = screen.getByTestId(`date-${dateStr}`);
|
|
120
|
+
|
|
121
|
+
await user.click(dateButton);
|
|
122
|
+
|
|
123
|
+
expect(handleSelect).toHaveBeenCalled();
|
|
124
|
+
expect(handleSelect.mock.calls[0][0]).toBeInstanceOf(Date);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('displays selected date', () => {
|
|
128
|
+
const selectedDate = new Date();
|
|
129
|
+
renderWithProviders(
|
|
130
|
+
<Calendar mode="single" selected={selectedDate} />
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const dateStr = selectedDate.toISOString().split('T')[0];
|
|
134
|
+
const dateButton = screen.getByTestId(`date-${dateStr}`);
|
|
135
|
+
expect(dateButton).toHaveAttribute('aria-selected', 'true');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Multiple date selection tests
|
|
140
|
+
describe('Multiple Date Selection', () => {
|
|
141
|
+
it('handles multiple date selection', async () => {
|
|
142
|
+
const handleSelect = vi.fn();
|
|
143
|
+
const user = userEvent.setup();
|
|
144
|
+
|
|
145
|
+
renderWithProviders(
|
|
146
|
+
<Calendar mode="multiple" selected={[]} onSelect={handleSelect} />
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const today = new Date();
|
|
150
|
+
const dateStr = today.toISOString().split('T')[0];
|
|
151
|
+
const dateButton = screen.getByTestId(`date-${dateStr}`);
|
|
152
|
+
|
|
153
|
+
await user.click(dateButton);
|
|
154
|
+
|
|
155
|
+
expect(handleSelect).toHaveBeenCalled();
|
|
156
|
+
expect(Array.isArray(handleSelect.mock.calls[0][0])).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('toggles date selection in multiple mode', async () => {
|
|
160
|
+
const TestComponent = () => {
|
|
161
|
+
const today = new Date();
|
|
162
|
+
const [selected, setSelected] = React.useState<Date[]>([today]);
|
|
163
|
+
const handleSelect = vi.fn((dates) => {
|
|
164
|
+
setSelected(dates);
|
|
165
|
+
return dates;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<Calendar mode="multiple" selected={selected} onSelect={handleSelect} />
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const user = userEvent.setup();
|
|
174
|
+
renderWithProviders(<TestComponent />);
|
|
175
|
+
|
|
176
|
+
const today = new Date();
|
|
177
|
+
const dateStr = today.toISOString().split('T')[0];
|
|
178
|
+
const dateButton = screen.getByTestId(`date-${dateStr}`);
|
|
179
|
+
|
|
180
|
+
expect(dateButton).toHaveAttribute('aria-selected', 'true');
|
|
181
|
+
|
|
182
|
+
await user.click(dateButton);
|
|
183
|
+
|
|
184
|
+
// After clicking, the date should be deselected (toggled off)
|
|
185
|
+
// Note: The mock handles the toggle logic
|
|
186
|
+
expect(dateButton).toBeInTheDocument();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Range date selection tests
|
|
191
|
+
describe('Range Date Selection', () => {
|
|
192
|
+
it('handles range date selection', async () => {
|
|
193
|
+
const handleSelect = vi.fn();
|
|
194
|
+
const user = userEvent.setup();
|
|
195
|
+
|
|
196
|
+
renderWithProviders(
|
|
197
|
+
<Calendar mode="range" selected={undefined} onSelect={handleSelect} />
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const today = new Date();
|
|
201
|
+
const dateStr = today.toISOString().split('T')[0];
|
|
202
|
+
const dateButton = screen.getByTestId(`date-${dateStr}`);
|
|
203
|
+
|
|
204
|
+
await user.click(dateButton);
|
|
205
|
+
|
|
206
|
+
expect(handleSelect).toHaveBeenCalled();
|
|
207
|
+
const selected = handleSelect.mock.calls[0][0];
|
|
208
|
+
expect(selected).toHaveProperty('from');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Disabled dates tests
|
|
213
|
+
describe('Disabled Dates', () => {
|
|
214
|
+
it('disables dates based on function', () => {
|
|
215
|
+
const disabledDate = new Date();
|
|
216
|
+
disabledDate.setDate(disabledDate.getDate() + 1);
|
|
217
|
+
|
|
218
|
+
renderWithProviders(
|
|
219
|
+
<Calendar
|
|
220
|
+
mode="single"
|
|
221
|
+
disabled={(date) => date > new Date()}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const dateStr = disabledDate.toISOString().split('T')[0];
|
|
226
|
+
const dateButton = screen.getByTestId(`date-${dateStr}`);
|
|
227
|
+
expect(dateButton).toBeDisabled();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('prevents selection of disabled dates', async () => {
|
|
231
|
+
const handleSelect = vi.fn();
|
|
232
|
+
const user = userEvent.setup();
|
|
233
|
+
|
|
234
|
+
const tomorrow = new Date();
|
|
235
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
236
|
+
|
|
237
|
+
renderWithProviders(
|
|
238
|
+
<Calendar
|
|
239
|
+
mode="single"
|
|
240
|
+
selected={undefined}
|
|
241
|
+
onSelect={handleSelect}
|
|
242
|
+
disabled={(date) => date > new Date()}
|
|
243
|
+
/>
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const dateStr = tomorrow.toISOString().split('T')[0];
|
|
247
|
+
const dateButton = screen.getByTestId(`date-${dateStr}`);
|
|
248
|
+
|
|
249
|
+
await user.click(dateButton);
|
|
250
|
+
|
|
251
|
+
expect(handleSelect).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('disables past dates', () => {
|
|
255
|
+
const yesterday = new Date();
|
|
256
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
257
|
+
|
|
258
|
+
renderWithProviders(
|
|
259
|
+
<Calendar
|
|
260
|
+
mode="single"
|
|
261
|
+
disabled={(date) => date < new Date()}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const dateStr = yesterday.toISOString().split('T')[0];
|
|
266
|
+
const dateButton = screen.getByTestId(`date-${dateStr}`);
|
|
267
|
+
expect(dateButton).toBeDisabled();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Accessibility tests
|
|
272
|
+
describe('Accessibility', () => {
|
|
273
|
+
it('has proper ARIA attributes', () => {
|
|
274
|
+
renderWithProviders(<Calendar mode="single" />);
|
|
275
|
+
const calendar = screen.getByTestId('calendar');
|
|
276
|
+
expect(calendar).toBeInTheDocument();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('marks selected dates with aria-selected', () => {
|
|
280
|
+
const selectedDate = new Date();
|
|
281
|
+
renderWithProviders(
|
|
282
|
+
<Calendar mode="single" selected={selectedDate} />
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const dateStr = selectedDate.toISOString().split('T')[0];
|
|
286
|
+
const dateButton = screen.getByTestId(`date-${dateStr}`);
|
|
287
|
+
expect(dateButton).toHaveAttribute('aria-selected', 'true');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('marks unselected dates with aria-selected="false"', () => {
|
|
291
|
+
const selectedDate = new Date();
|
|
292
|
+
const unselectedDate = new Date();
|
|
293
|
+
unselectedDate.setDate(selectedDate.getDate() + 1);
|
|
294
|
+
|
|
295
|
+
renderWithProviders(
|
|
296
|
+
<Calendar mode="single" selected={selectedDate} />
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const dateStr = unselectedDate.toISOString().split('T')[0];
|
|
300
|
+
const dateButton = screen.getByTestId(`date-${dateStr}`);
|
|
301
|
+
expect(dateButton).toHaveAttribute('aria-selected', 'false');
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Integration tests
|
|
306
|
+
describe('Integration', () => {
|
|
307
|
+
it('works with controlled state', () => {
|
|
308
|
+
const TestComponent = () => {
|
|
309
|
+
const [date, setDate] = React.useState<Date | undefined>(undefined);
|
|
310
|
+
return (
|
|
311
|
+
<div>
|
|
312
|
+
<Calendar mode="single" selected={date} onSelect={setDate} />
|
|
313
|
+
<div data-testid="selected-date">
|
|
314
|
+
{date ? date.toISOString().split('T')[0] : 'No date selected'}
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
renderWithProviders(<TestComponent />);
|
|
321
|
+
|
|
322
|
+
expect(screen.getByTestId('selected-date')).toHaveTextContent('No date selected');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('handles undefined selected prop gracefully', () => {
|
|
326
|
+
renderWithProviders(<Calendar mode="single" selected={undefined} />);
|
|
327
|
+
const calendar = screen.getByTestId('calendar');
|
|
328
|
+
expect(calendar).toBeInTheDocument();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('handles null selected prop gracefully', () => {
|
|
332
|
+
renderWithProviders(<Calendar mode="single" selected={null as any} />);
|
|
333
|
+
const calendar = screen.getByTestId('calendar');
|
|
334
|
+
expect(calendar).toBeInTheDocument();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Calendar Component
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/Calendar
|
|
5
|
+
* @since 0.5.141
|
|
6
|
+
*
|
|
7
|
+
* A date picker calendar component built on react-day-picker.
|
|
8
|
+
* Provides accessible date selection with keyboard navigation and ARIA support.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Single, range, and multiple date selection modes
|
|
12
|
+
* - Date disabling (past dates, weekends, etc.)
|
|
13
|
+
* - Localization support
|
|
14
|
+
* - Keyboard navigation
|
|
15
|
+
* - Accessible date selection
|
|
16
|
+
* - Customizable styling
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* // Single date selection
|
|
21
|
+
* <Calendar
|
|
22
|
+
* mode="single"
|
|
23
|
+
* selected={date}
|
|
24
|
+
* onSelect={setDate}
|
|
25
|
+
* />
|
|
26
|
+
*
|
|
27
|
+
* // Date range selection
|
|
28
|
+
* <Calendar
|
|
29
|
+
* mode="range"
|
|
30
|
+
* selected={dateRange}
|
|
31
|
+
* onSelect={setDateRange}
|
|
32
|
+
* />
|
|
33
|
+
*
|
|
34
|
+
* // Multiple date selection
|
|
35
|
+
* <Calendar
|
|
36
|
+
* mode="multiple"
|
|
37
|
+
* selected={dates}
|
|
38
|
+
* onSelect={setDates}
|
|
39
|
+
* />
|
|
40
|
+
*
|
|
41
|
+
* // With disabled dates
|
|
42
|
+
* <Calendar
|
|
43
|
+
* mode="single"
|
|
44
|
+
* selected={date}
|
|
45
|
+
* onSelect={setDate}
|
|
46
|
+
* disabled={(date) => date < new Date()}
|
|
47
|
+
* />
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @accessibility
|
|
51
|
+
* - WCAG 2.1 AA compliant
|
|
52
|
+
* - Keyboard navigation (Arrow keys, Page Up/Down, Home, End)
|
|
53
|
+
* - Screen reader support with proper ARIA attributes
|
|
54
|
+
* - Focus management
|
|
55
|
+
* - Date announcements
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import * as React from 'react';
|
|
59
|
+
import { DayPicker, type DayPickerProps } from 'react-day-picker';
|
|
60
|
+
import { cn } from '../../utils/core/cn';
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// CALENDAR COMPONENT
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
export interface CalendarProps extends Omit<DayPickerProps, 'className' | 'classNames' | 'styles'> {
|
|
67
|
+
/**
|
|
68
|
+
* Additional CSS classes to apply to the calendar wrapper
|
|
69
|
+
*/
|
|
70
|
+
className?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Custom classNames for DayPicker sub-components
|
|
73
|
+
*/
|
|
74
|
+
classNames?: DayPickerProps['classNames'];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Calendar component
|
|
79
|
+
* A flexible, accessible calendar component for date selection.
|
|
80
|
+
* Built on react-day-picker with pace-core styling.
|
|
81
|
+
*
|
|
82
|
+
* @param props - Calendar configuration and styling
|
|
83
|
+
* @param ref - Forwarded ref (not used directly, but maintained for API consistency)
|
|
84
|
+
* @returns JSX.Element - The rendered calendar element
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* // Single date selection
|
|
89
|
+
* <Calendar
|
|
90
|
+
* mode="single"
|
|
91
|
+
* selected={date}
|
|
92
|
+
* onSelect={setDate}
|
|
93
|
+
* />
|
|
94
|
+
*
|
|
95
|
+
* // With disabled dates
|
|
96
|
+
* <Calendar
|
|
97
|
+
* mode="single"
|
|
98
|
+
* selected={date}
|
|
99
|
+
* onSelect={setDate}
|
|
100
|
+
* disabled={(date) => date < new Date()}
|
|
101
|
+
* />
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
|
|
105
|
+
({ className, classNames, mode, ...props }, ref) => {
|
|
106
|
+
return (
|
|
107
|
+
<div ref={ref} className={cn('p-3', className)}>
|
|
108
|
+
<DayPicker
|
|
109
|
+
mode={mode}
|
|
110
|
+
className="rounded-md border border-sec-200 bg-background"
|
|
111
|
+
classNames={{
|
|
112
|
+
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
|
|
113
|
+
month: 'space-y-4',
|
|
114
|
+
caption: 'flex justify-center pt-1 relative items-center',
|
|
115
|
+
caption_label: 'text-sm font-medium',
|
|
116
|
+
nav: 'space-x-1 flex items-center',
|
|
117
|
+
nav_button: cn(
|
|
118
|
+
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
|
119
|
+
'border border-input hover:bg-acc-100',
|
|
120
|
+
'inline-flex items-center justify-center rounded-md',
|
|
121
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2'
|
|
122
|
+
),
|
|
123
|
+
nav_button_previous: 'absolute left-1',
|
|
124
|
+
nav_button_next: 'absolute right-1',
|
|
125
|
+
table: 'w-full border-collapse space-y-1',
|
|
126
|
+
head_row: 'flex',
|
|
127
|
+
head_cell: 'text-sec-600 rounded-md w-9 font-normal text-[0.8rem]',
|
|
128
|
+
row: 'flex w-full mt-2',
|
|
129
|
+
cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-acc-50/50 [&:has([aria-selected])]:bg-acc-100 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
|
|
130
|
+
day: cn(
|
|
131
|
+
'h-9 w-9 p-0 font-normal aria-selected:opacity-100',
|
|
132
|
+
'hover:bg-acc-100 hover:text-main-600',
|
|
133
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2',
|
|
134
|
+
'inline-flex items-center justify-center rounded-md'
|
|
135
|
+
),
|
|
136
|
+
day_range_end: 'day-range-end',
|
|
137
|
+
day_selected: 'bg-main-600 text-main-50 hover:bg-main-600 hover:text-main-50 focus:bg-main-600 focus:text-main-50',
|
|
138
|
+
day_today: 'bg-sec-100 text-main-600 font-semibold',
|
|
139
|
+
day_outside: 'day-outside text-sec-400 opacity-50 aria-selected:bg-acc-50/50 aria-selected:text-sec-400 aria-selected:opacity-30',
|
|
140
|
+
day_disabled: 'text-sec-400 opacity-50 cursor-not-allowed',
|
|
141
|
+
day_range_middle: 'aria-selected:bg-acc-100 aria-selected:text-main-600',
|
|
142
|
+
day_hidden: 'invisible',
|
|
143
|
+
...classNames,
|
|
144
|
+
}}
|
|
145
|
+
components={{
|
|
146
|
+
IconLeft: ({ ...props }) => (
|
|
147
|
+
<svg
|
|
148
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
149
|
+
viewBox="0 0 24 24"
|
|
150
|
+
fill="none"
|
|
151
|
+
stroke="currentColor"
|
|
152
|
+
strokeWidth="2"
|
|
153
|
+
strokeLinecap="round"
|
|
154
|
+
strokeLinejoin="round"
|
|
155
|
+
className="h-4 w-4"
|
|
156
|
+
{...props}
|
|
157
|
+
>
|
|
158
|
+
<path d="m15 18-6-6 6-6" />
|
|
159
|
+
</svg>
|
|
160
|
+
),
|
|
161
|
+
IconRight: ({ ...props }) => (
|
|
162
|
+
<svg
|
|
163
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
164
|
+
viewBox="0 0 24 24"
|
|
165
|
+
fill="none"
|
|
166
|
+
stroke="currentColor"
|
|
167
|
+
strokeWidth="2"
|
|
168
|
+
strokeLinecap="round"
|
|
169
|
+
strokeLinejoin="round"
|
|
170
|
+
className="h-4 w-4"
|
|
171
|
+
{...props}
|
|
172
|
+
>
|
|
173
|
+
<path d="m9 18 6-6-6-6" />
|
|
174
|
+
</svg>
|
|
175
|
+
),
|
|
176
|
+
...props.components,
|
|
177
|
+
}}
|
|
178
|
+
{...(props as any)}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
Calendar.displayName = 'Calendar';
|
|
186
|
+
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// EXPORTS
|
|
189
|
+
// ============================================================================
|
|
190
|
+
|
|
191
|
+
export { Calendar };
|
|
192
|
+
|