@jmruthers/pace-core 0.5.183 → 0.5.185
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/CHANGELOG.md +38 -0
- package/README.md +60 -1
- package/core-usage-manifest.json +312 -0
- package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
- package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
- package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-BABf6JCh.d.ts} +21 -10
- package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
- package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
- package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
- package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
- package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
- package/dist/{chunk-CSOFYHAG.js → chunk-AISXLWGZ.js} +374 -60
- package/dist/chunk-AISXLWGZ.js.map +1 -0
- package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
- package/dist/chunk-FXFJRTKI.js.map +1 -0
- package/dist/{chunk-QETLRQI6.js → chunk-HC67NW5K.js} +380 -360
- package/dist/chunk-HC67NW5K.js.map +1 -0
- package/dist/chunk-HESYZWZW.js +388 -0
- package/dist/chunk-HESYZWZW.js.map +1 -0
- package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
- package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
- package/dist/{chunk-UHNYIBXL.js → chunk-IXSNYUCT.js} +1 -1
- package/dist/chunk-IXSNYUCT.js.map +1 -0
- package/dist/{chunk-MI7HBHN3.js → chunk-MX3EIJGQ.js} +4 -3
- package/dist/{chunk-MI7HBHN3.js.map → chunk-MX3EIJGQ.js.map} +1 -1
- package/dist/{chunk-PWAHJW4G.js → chunk-OKI34GZD.js} +86 -33
- package/dist/chunk-OKI34GZD.js.map +1 -0
- package/dist/{chunk-W22JP75J.js → chunk-STTZQK2I.js} +3 -3
- package/dist/chunk-THRPYOFK.js +215 -0
- package/dist/chunk-THRPYOFK.js.map +1 -0
- package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
- package/dist/chunk-U6WNSFX5.js.map +1 -0
- package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
- package/dist/components.d.ts +182 -6
- package/dist/components.js +157 -11
- package/dist/components.js.map +1 -1
- package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
- package/dist/{file-reference-D06mEEWW.d.ts → file-reference-BjR39ktt.d.ts} +7 -1
- package/dist/hooks.d.ts +7 -14
- package/dist/hooks.js +10 -22
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.js +79 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +3 -1
- package/dist/rbac/index.d.ts +205 -14
- package/dist/rbac/index.js +28 -6
- package/dist/timezone-_pgH8qrY.d.ts +530 -0
- package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-CvnC3d-e.d.ts} +113 -2
- package/dist/utils.d.ts +109 -151
- package/dist/utils.js +128 -138
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +60 -1
- 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/Logger.md +178 -0
- 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/RBACAuditManager.md +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +5 -5
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +54 -0
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.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 +18 -2
- 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/ComplianceResult.md +30 -0
- 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/DatabaseComplianceResult.md +85 -0
- package/docs/api/interfaces/DatabaseIssue.md +41 -0
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +6 -6
- 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 +24 -8
- package/docs/api/interfaces/FileUploadProps.md +24 -13
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
- 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/LoggerConfig.md +62 -0
- 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 +36 -23
- 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 +11 -11
- 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/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.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/QuickFix.md +52 -0
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +4 -4
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +5 -5
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
- 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/SetupIssue.md +41 -0
- 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 +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.md +1 -1
- 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/UseFormDialogOptions.md +62 -0
- package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- package/docs/api/interfaces/UsePublicEventLogoReturn.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 +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- 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 +738 -42
- package/docs/api-reference/hooks.md +111 -0
- package/docs/api-reference/rpc-functions.md +1 -1
- package/docs/api-reference/utilities.md +184 -0
- package/docs/getting-started/installation-guide.md +75 -16
- package/docs/getting-started/quick-start.md +61 -11
- package/docs/implementation-guides/authentication.md +88 -12
- package/docs/implementation-guides/file-reference-system.md +2 -1
- package/docs/implementation-guides/file-upload-storage.md +21 -0
- package/docs/rbac/README.md +1 -0
- package/docs/rbac/compliance/compliance-guide.md +544 -0
- package/docs/rbac/getting-started.md +158 -33
- package/docs/standards/pace-core-compliance.md +432 -0
- package/eslint-config-pace-core.cjs +93 -0
- package/package.json +15 -3
- package/scripts/analyze-bundle.js +232 -0
- package/scripts/build-css.js +56 -0
- package/scripts/build-docs-incremental.js +1015 -0
- package/scripts/check-pace-core-compliance.cjs +2353 -0
- package/scripts/generate-docs.js +157 -0
- package/scripts/setup-build-cache.js +73 -0
- package/scripts/utils/command-runner.js +131 -0
- package/scripts/utils/env.js +33 -0
- package/scripts/utils/index.js +10 -0
- package/scripts/utils/logger.js +88 -0
- package/scripts/utils/path-helpers.js +37 -0
- package/scripts/validate-formats.js +133 -0
- package/scripts/validate-master.js +155 -0
- package/scripts/validate-pre-publish.js +140 -0
- package/scripts/validate-theme.js +142 -0
- package/src/components/Calendar/Calendar.tsx +8 -1
- package/src/components/Card/Card.tsx +47 -8
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
- package/src/components/DatePickerWithTimezone/README.md +135 -0
- package/src/components/DatePickerWithTimezone/index.ts +10 -0
- package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
- package/src/components/DateTimeField/DateTimeField.tsx +232 -0
- package/src/components/DateTimeField/README.md +148 -0
- package/src/components/DateTimeField/index.ts +10 -0
- package/src/components/FileUpload/FileUpload.tsx +3 -0
- package/src/components/Header/Header.test.tsx +47 -18
- package/src/components/Header/Header.tsx +24 -6
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
- package/src/components/PaceAppLayout/README.md +9 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +12 -4
- package/src/components/index.ts +8 -0
- package/src/eslint-rules/pace-core-compliance.cjs +406 -0
- package/src/eslint-rules/pace-core-compliance.js +640 -0
- package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useFileReference.test.ts +1 -0
- package/src/hooks/useFormDialog.ts +147 -0
- package/src/index.ts +27 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
- package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
- package/src/rbac/compliance/database-validator.ts +165 -0
- package/src/rbac/compliance/index.ts +38 -0
- package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
- package/src/rbac/compliance/runtime-compliance.ts +77 -0
- package/src/rbac/compliance/setup-validator.ts +131 -0
- package/src/rbac/components/PagePermissionGuard.tsx +8 -64
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
- package/src/rbac/docs/event-based-apps.md +285 -0
- package/src/rbac/errors.ts +11 -0
- package/src/rbac/hooks/useRoleManagement.ts +292 -12
- package/src/rbac/index.ts +30 -0
- package/src/services/OrganisationService.ts +4 -0
- package/src/types/file-reference.ts +6 -0
- package/src/utils/__tests__/timezone.test.ts +345 -0
- package/src/utils/file-reference/__tests__/file-reference.test.ts +2 -0
- package/src/utils/file-reference/index.ts +1 -0
- package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
- package/src/utils/formatting/formatting.ts +179 -0
- package/src/utils/index.ts +27 -1
- package/src/utils/location/index.ts +16 -0
- package/src/utils/location/location.test.ts +286 -0
- package/src/utils/location/location.ts +175 -0
- package/src/utils/timezone/index.ts +17 -0
- package/src/utils/timezone/timezone.test.ts +349 -0
- package/src/utils/timezone/timezone.ts +281 -0
- package/dist/chunk-CSOFYHAG.js.map +0 -1
- package/dist/chunk-FUEYYMX5.js.map +0 -1
- package/dist/chunk-HKIT6O7W.js +0 -198
- package/dist/chunk-HKIT6O7W.js.map +0 -1
- package/dist/chunk-KUEN3HFB.js +0 -94
- package/dist/chunk-KUEN3HFB.js.map +0 -1
- package/dist/chunk-M7W4CP3M.js.map +0 -1
- package/dist/chunk-PWAHJW4G.js.map +0 -1
- package/dist/chunk-QETLRQI6.js.map +0 -1
- package/dist/chunk-UHNYIBXL.js.map +0 -1
- package/dist/formatting-5wETwiGF.d.ts +0 -162
- /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
- /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
- /package/dist/{chunk-W22JP75J.js.map → chunk-STTZQK2I.js.map} +0 -0
- /package/dist/{chunk-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file DateTimeField Component Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DateTimeField/__tests__
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive test suite for DateTimeField component.
|
|
8
|
+
* Tests cover all major functionality, edge cases, and accessibility.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
13
|
+
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
15
|
+
import { DateTimeField } from './DateTimeField';
|
|
16
|
+
|
|
17
|
+
// Mock timezone utilities
|
|
18
|
+
vi.mock('../../utils/timezone', () => ({
|
|
19
|
+
toZonedTime: vi.fn((date: Date, tz: string) => {
|
|
20
|
+
// Simple mock: return date as-is for testing
|
|
21
|
+
return date;
|
|
22
|
+
}),
|
|
23
|
+
fromZonedTime: vi.fn((date: Date, tz: string) => {
|
|
24
|
+
// Simple mock: return date as-is for testing
|
|
25
|
+
return date;
|
|
26
|
+
}),
|
|
27
|
+
getUserTimeZone: vi.fn(() => 'America/New_York')
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
describe('DateTimeField Component', () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('Rendering', () => {
|
|
36
|
+
it('renders with default props', () => {
|
|
37
|
+
const onChange = vi.fn();
|
|
38
|
+
render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
|
|
39
|
+
|
|
40
|
+
expect(screen.getByText('Start Time')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByLabelText(/Start Time/)).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders with required indicator', () => {
|
|
45
|
+
const onChange = vi.fn();
|
|
46
|
+
render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} required />);
|
|
47
|
+
|
|
48
|
+
const label = screen.getByText('Start Time');
|
|
49
|
+
expect(label).toBeInTheDocument();
|
|
50
|
+
// Required indicator should be present
|
|
51
|
+
expect(screen.getByLabelText(/required/i)).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders with helper text', () => {
|
|
55
|
+
const onChange = vi.fn();
|
|
56
|
+
render(
|
|
57
|
+
<DateTimeField
|
|
58
|
+
label="Start Time"
|
|
59
|
+
value={undefined}
|
|
60
|
+
onChange={onChange}
|
|
61
|
+
helperText="Select a start time"
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(screen.getByText('Select a start time')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('renders with error message', () => {
|
|
69
|
+
const onChange = vi.fn();
|
|
70
|
+
render(
|
|
71
|
+
<DateTimeField
|
|
72
|
+
label="Start Time"
|
|
73
|
+
value={undefined}
|
|
74
|
+
onChange={onChange}
|
|
75
|
+
error="Invalid date"
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(screen.getByText('Invalid date')).toBeInTheDocument();
|
|
80
|
+
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('renders with custom id', () => {
|
|
84
|
+
const onChange = vi.fn();
|
|
85
|
+
render(
|
|
86
|
+
<DateTimeField
|
|
87
|
+
id="custom-id"
|
|
88
|
+
label="Start Time"
|
|
89
|
+
value={undefined}
|
|
90
|
+
onChange={onChange}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(screen.getByLabelText(/Start Time/)).toHaveAttribute('id', 'custom-id');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('Value Display and Conversion', () => {
|
|
99
|
+
it('displays ISO string value correctly', () => {
|
|
100
|
+
const onChange = vi.fn();
|
|
101
|
+
const value = '2024-01-15T10:00:00Z';
|
|
102
|
+
render(<DateTimeField label="Start Time" value={value} onChange={onChange} />);
|
|
103
|
+
|
|
104
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
105
|
+
expect(input.value).toBeTruthy();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('displays Date object value correctly', () => {
|
|
109
|
+
const onChange = vi.fn();
|
|
110
|
+
const value = new Date('2024-01-15T10:00:00Z');
|
|
111
|
+
render(<DateTimeField label="Start Time" value={value} onChange={onChange} />);
|
|
112
|
+
|
|
113
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
114
|
+
expect(input.value).toBeTruthy();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('handles undefined value', () => {
|
|
118
|
+
const onChange = vi.fn();
|
|
119
|
+
render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
|
|
120
|
+
|
|
121
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
122
|
+
expect(input.value).toBe('');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('displays timezone information when not UTC', () => {
|
|
126
|
+
const onChange = vi.fn();
|
|
127
|
+
render(
|
|
128
|
+
<DateTimeField
|
|
129
|
+
label="Start Time"
|
|
130
|
+
value={undefined}
|
|
131
|
+
onChange={onChange}
|
|
132
|
+
timezone="Europe/London"
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(screen.getByText('Europe/London')).toBeInTheDocument();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('displays "Local" when timezone matches user timezone', () => {
|
|
140
|
+
const onChange = vi.fn();
|
|
141
|
+
render(
|
|
142
|
+
<DateTimeField
|
|
143
|
+
label="Start Time"
|
|
144
|
+
value={undefined}
|
|
145
|
+
onChange={onChange}
|
|
146
|
+
timezone="America/New_York"
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(screen.getByText('Local')).toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('does not display timezone for UTC', () => {
|
|
154
|
+
const onChange = vi.fn();
|
|
155
|
+
const { container } = render(
|
|
156
|
+
<DateTimeField
|
|
157
|
+
label="Start Time"
|
|
158
|
+
value={undefined}
|
|
159
|
+
onChange={onChange}
|
|
160
|
+
timezone="UTC"
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const timezoneDisplay = container.querySelector('.text-muted-foreground');
|
|
165
|
+
expect(timezoneDisplay).not.toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('onChange Behavior', () => {
|
|
170
|
+
it('calls onChange with ISO string when returnAsDate is false', async () => {
|
|
171
|
+
const onChange = vi.fn();
|
|
172
|
+
const user = userEvent.setup();
|
|
173
|
+
|
|
174
|
+
render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
|
|
175
|
+
|
|
176
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
177
|
+
await user.type(input, '2024-01-15T10:00');
|
|
178
|
+
|
|
179
|
+
await waitFor(() => {
|
|
180
|
+
expect(onChange).toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('calls onChange with Date object when returnAsDate is true', async () => {
|
|
185
|
+
const onChange = vi.fn();
|
|
186
|
+
const user = userEvent.setup();
|
|
187
|
+
|
|
188
|
+
render(
|
|
189
|
+
<DateTimeField
|
|
190
|
+
label="Start Time"
|
|
191
|
+
value={undefined}
|
|
192
|
+
onChange={onChange}
|
|
193
|
+
returnAsDate
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
198
|
+
await user.type(input, '2024-01-15T10:00');
|
|
199
|
+
|
|
200
|
+
await waitFor(() => {
|
|
201
|
+
expect(onChange).toHaveBeenCalled();
|
|
202
|
+
const callArg = onChange.mock.calls[onChange.mock.calls.length - 1][0];
|
|
203
|
+
expect(callArg).toBeInstanceOf(Date);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('calls onChange with undefined when input is cleared', async () => {
|
|
208
|
+
const onChange = vi.fn();
|
|
209
|
+
const user = userEvent.setup();
|
|
210
|
+
|
|
211
|
+
// Set an initial value first
|
|
212
|
+
const { rerender } = render(
|
|
213
|
+
<DateTimeField label="Start Time" value="2024-01-15T10:00:00Z" onChange={onChange} />
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
217
|
+
await user.clear(input);
|
|
218
|
+
|
|
219
|
+
// Trigger change event manually to simulate clearing
|
|
220
|
+
fireEvent.change(input, { target: { value: '' } });
|
|
221
|
+
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
expect(onChange).toHaveBeenCalledWith(undefined);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('User Editing', () => {
|
|
229
|
+
it('prevents unwanted conversions during editing', async () => {
|
|
230
|
+
const onChange = vi.fn();
|
|
231
|
+
const user = userEvent.setup();
|
|
232
|
+
const value = '2024-01-15T10:00:00Z';
|
|
233
|
+
|
|
234
|
+
render(<DateTimeField label="Start Time" value={value} onChange={onChange} />);
|
|
235
|
+
|
|
236
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
237
|
+
await user.click(input);
|
|
238
|
+
await user.type(input, '2024-01-16');
|
|
239
|
+
|
|
240
|
+
// Should not trigger multiple onChange calls during typing
|
|
241
|
+
await waitFor(() => {
|
|
242
|
+
expect(onChange).toHaveBeenCalled();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('handles blur event correctly', async () => {
|
|
247
|
+
const onChange = vi.fn();
|
|
248
|
+
const user = userEvent.setup();
|
|
249
|
+
|
|
250
|
+
render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
|
|
251
|
+
|
|
252
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
253
|
+
await user.click(input);
|
|
254
|
+
await user.tab();
|
|
255
|
+
|
|
256
|
+
// Blur should stop editing mode
|
|
257
|
+
expect(input).not.toHaveFocus();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('Edge Cases', () => {
|
|
262
|
+
it('handles invalid date string gracefully', () => {
|
|
263
|
+
const onChange = vi.fn();
|
|
264
|
+
render(<DateTimeField label="Start Time" value="invalid-date" onChange={onChange} />);
|
|
265
|
+
|
|
266
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
267
|
+
expect(input.value).toBe('');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('handles null value gracefully', () => {
|
|
271
|
+
const onChange = vi.fn();
|
|
272
|
+
// @ts-expect-error - Testing edge case
|
|
273
|
+
render(<DateTimeField label="Start Time" value={null} onChange={onChange} />);
|
|
274
|
+
|
|
275
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
276
|
+
expect(input.value).toBe('');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('handles empty string value', () => {
|
|
280
|
+
const onChange = vi.fn();
|
|
281
|
+
render(<DateTimeField label="Start Time" value="" onChange={onChange} />);
|
|
282
|
+
|
|
283
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
284
|
+
expect(input.value).toBe('');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('Accessibility', () => {
|
|
289
|
+
it('associates label with input via htmlFor', () => {
|
|
290
|
+
const onChange = vi.fn();
|
|
291
|
+
render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
|
|
292
|
+
|
|
293
|
+
const label = screen.getByText('Start Time');
|
|
294
|
+
const input = screen.getByLabelText(/Start Time/);
|
|
295
|
+
|
|
296
|
+
expect(label).toBeInTheDocument();
|
|
297
|
+
expect(input).toBeInTheDocument();
|
|
298
|
+
expect(input).toHaveAttribute('id');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('supports keyboard navigation', async () => {
|
|
302
|
+
const onChange = vi.fn();
|
|
303
|
+
const user = userEvent.setup();
|
|
304
|
+
|
|
305
|
+
render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
|
|
306
|
+
|
|
307
|
+
const input = screen.getByLabelText(/Start Time/);
|
|
308
|
+
await user.tab();
|
|
309
|
+
|
|
310
|
+
expect(input).toHaveFocus();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('announces errors to screen readers', () => {
|
|
314
|
+
const onChange = vi.fn();
|
|
315
|
+
render(
|
|
316
|
+
<DateTimeField
|
|
317
|
+
label="Start Time"
|
|
318
|
+
value={undefined}
|
|
319
|
+
onChange={onChange}
|
|
320
|
+
error="Invalid date"
|
|
321
|
+
/>
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const errorMessage = screen.getByRole('alert');
|
|
325
|
+
expect(errorMessage).toBeInTheDocument();
|
|
326
|
+
expect(errorMessage).toHaveTextContent('Invalid date');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('indicates required field to screen readers', () => {
|
|
330
|
+
const onChange = vi.fn();
|
|
331
|
+
render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} required />);
|
|
332
|
+
|
|
333
|
+
const requiredIndicator = screen.getByLabelText(/required/i);
|
|
334
|
+
expect(requiredIndicator).toBeInTheDocument();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('Controlled vs Uncontrolled', () => {
|
|
339
|
+
it('works as controlled component', () => {
|
|
340
|
+
const onChange = vi.fn();
|
|
341
|
+
const value = '2024-01-15T10:00:00Z';
|
|
342
|
+
|
|
343
|
+
const { rerender } = render(
|
|
344
|
+
<DateTimeField label="Start Time" value={value} onChange={onChange} />
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
|
|
348
|
+
const initialValue = input.value;
|
|
349
|
+
|
|
350
|
+
rerender(
|
|
351
|
+
<DateTimeField label="Start Time" value="2024-01-16T10:00:00Z" onChange={onChange} />
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
expect(input.value).not.toBe(initialValue);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file DateTimeField Component
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DateTimeField
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Form input component for datetime values with timezone support.
|
|
8
|
+
* Handles UTC ↔ timezone conversion automatically.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Automatic UTC ↔ timezone conversion
|
|
12
|
+
* - Prevents unwanted conversions during user editing
|
|
13
|
+
* - Shows timezone information when not UTC
|
|
14
|
+
* - Supports both ISO string and Date object values
|
|
15
|
+
* - Uses native datetime-local input type
|
|
16
|
+
* - Accessible form field with proper labels
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* import { DateTimeField } from '@jmruthers/pace-core/components';
|
|
21
|
+
* import { useState } from 'react';
|
|
22
|
+
*
|
|
23
|
+
* function EventForm() {
|
|
24
|
+
* const [startTime, setStartTime] = useState<string>();
|
|
25
|
+
*
|
|
26
|
+
* return (
|
|
27
|
+
* <DateTimeField
|
|
28
|
+
* label="Start Time"
|
|
29
|
+
* value={startTime}
|
|
30
|
+
* onChange={setStartTime}
|
|
31
|
+
* timezone="America/New_York"
|
|
32
|
+
* required
|
|
33
|
+
* />
|
|
34
|
+
* );
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @accessibility
|
|
39
|
+
* - Proper label association with htmlFor
|
|
40
|
+
* - Required field indicators
|
|
41
|
+
* - Screen reader friendly
|
|
42
|
+
* - Keyboard navigation support
|
|
43
|
+
* - Focus management
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import * as React from 'react';
|
|
47
|
+
import { format, parse } from 'date-fns';
|
|
48
|
+
import { Label } from '../Label';
|
|
49
|
+
import { Input } from '../Input';
|
|
50
|
+
import { cn } from '../../utils/core/cn';
|
|
51
|
+
import { toZonedTime, fromZonedTime, getUserTimeZone } from '../../utils/timezone';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Props for the DateTimeField component
|
|
55
|
+
*/
|
|
56
|
+
export interface DateTimeFieldProps {
|
|
57
|
+
/**
|
|
58
|
+
* Field label
|
|
59
|
+
*/
|
|
60
|
+
label: string;
|
|
61
|
+
/**
|
|
62
|
+
* UTC date value (ISO string, Date object, or undefined)
|
|
63
|
+
*/
|
|
64
|
+
value: string | Date | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Change handler that receives UTC value (ISO string or Date object)
|
|
67
|
+
*/
|
|
68
|
+
onChange: (value: string | Date | undefined) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Target timezone for display (default: 'UTC')
|
|
71
|
+
*/
|
|
72
|
+
timezone?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Whether the field is required
|
|
75
|
+
*/
|
|
76
|
+
required?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Additional CSS classes
|
|
79
|
+
*/
|
|
80
|
+
className?: string;
|
|
81
|
+
/**
|
|
82
|
+
* If true, onChange returns Date object instead of ISO string
|
|
83
|
+
*/
|
|
84
|
+
returnAsDate?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Input id (auto-generated if not provided)
|
|
87
|
+
*/
|
|
88
|
+
id?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Helper text to display below the label
|
|
91
|
+
*/
|
|
92
|
+
helperText?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Error message to display
|
|
95
|
+
*/
|
|
96
|
+
error?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* DateTimeField component
|
|
101
|
+
* Form input for datetime values with automatic timezone conversion
|
|
102
|
+
*
|
|
103
|
+
* @param props - DateTimeField configuration
|
|
104
|
+
* @returns JSX.Element - The rendered datetime field
|
|
105
|
+
*/
|
|
106
|
+
export function DateTimeField({
|
|
107
|
+
label,
|
|
108
|
+
value,
|
|
109
|
+
onChange,
|
|
110
|
+
timezone = 'UTC',
|
|
111
|
+
required = false,
|
|
112
|
+
className,
|
|
113
|
+
returnAsDate = false,
|
|
114
|
+
id,
|
|
115
|
+
helperText,
|
|
116
|
+
error
|
|
117
|
+
}: DateTimeFieldProps) {
|
|
118
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
119
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
120
|
+
const fieldId = id || `datetime-field-${React.useId()}`;
|
|
121
|
+
|
|
122
|
+
// Convert UTC value to timezone for display
|
|
123
|
+
const getDisplayValue = React.useCallback((): string => {
|
|
124
|
+
if (!value) {
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
let dateObj: Date;
|
|
130
|
+
if (typeof value === 'string') {
|
|
131
|
+
dateObj = new Date(value);
|
|
132
|
+
} else {
|
|
133
|
+
dateObj = value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!dateObj || isNaN(dateObj.getTime())) {
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Convert UTC to timezone
|
|
141
|
+
const zonedDate = toZonedTime(dateObj, timezone);
|
|
142
|
+
|
|
143
|
+
// Format for datetime-local input (YYYY-MM-DDTHH:mm)
|
|
144
|
+
return format(zonedDate, "yyyy-MM-dd'T'HH:mm");
|
|
145
|
+
} catch {
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
}, [value, timezone]);
|
|
149
|
+
|
|
150
|
+
const displayValue = isEditing ? undefined : getDisplayValue();
|
|
151
|
+
|
|
152
|
+
// Handle input change
|
|
153
|
+
const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
154
|
+
setIsEditing(true);
|
|
155
|
+
const inputValue = e.target.value;
|
|
156
|
+
|
|
157
|
+
if (!inputValue) {
|
|
158
|
+
onChange(undefined);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
// Parse the datetime-local value (in timezone)
|
|
164
|
+
const localDate = parse(inputValue, "yyyy-MM-dd'T'HH:mm", new Date());
|
|
165
|
+
|
|
166
|
+
if (isNaN(localDate.getTime())) {
|
|
167
|
+
onChange(undefined);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Convert from timezone to UTC
|
|
172
|
+
const utcDate = fromZonedTime(localDate, timezone);
|
|
173
|
+
|
|
174
|
+
// Return as ISO string or Date object
|
|
175
|
+
if (returnAsDate) {
|
|
176
|
+
onChange(utcDate);
|
|
177
|
+
} else {
|
|
178
|
+
onChange(utcDate.toISOString());
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
onChange(undefined);
|
|
182
|
+
}
|
|
183
|
+
}, [timezone, returnAsDate, onChange]);
|
|
184
|
+
|
|
185
|
+
// Handle blur to stop editing mode
|
|
186
|
+
const handleBlur = React.useCallback(() => {
|
|
187
|
+
setIsEditing(false);
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
// Get timezone display text
|
|
191
|
+
const getTimezoneDisplay = React.useCallback((): string => {
|
|
192
|
+
if (timezone === 'UTC') {
|
|
193
|
+
return '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const userTz = getUserTimeZone();
|
|
197
|
+
if (timezone === userTz) {
|
|
198
|
+
return 'Local';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return timezone;
|
|
202
|
+
}, [timezone]);
|
|
203
|
+
|
|
204
|
+
const timezoneDisplay = getTimezoneDisplay();
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div className={cn('space-y-2', className)}>
|
|
208
|
+
<Label htmlFor={fieldId} required={required} helperText={helperText} error={error}>
|
|
209
|
+
{label}
|
|
210
|
+
</Label>
|
|
211
|
+
<div className="relative">
|
|
212
|
+
<Input
|
|
213
|
+
ref={inputRef}
|
|
214
|
+
id={fieldId}
|
|
215
|
+
type="datetime-local"
|
|
216
|
+
value={displayValue}
|
|
217
|
+
onChange={handleChange}
|
|
218
|
+
onBlur={handleBlur}
|
|
219
|
+
required={required}
|
|
220
|
+
error={!!error}
|
|
221
|
+
className="w-full"
|
|
222
|
+
/>
|
|
223
|
+
{timezoneDisplay && (
|
|
224
|
+
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none">
|
|
225
|
+
{timezoneDisplay}
|
|
226
|
+
</span>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|