@jmruthers/pace-core 0.5.184 → 0.5.186
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-DIzEzwKl.d.ts} +23 -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-W22JP75J.js → chunk-DAGICKHT.js} +9 -7
- package/dist/chunk-DAGICKHT.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-CSOFYHAG.js → chunk-GRIQLQ52.js} +374 -60
- package/dist/chunk-GRIQLQ52.js.map +1 -0
- package/dist/{chunk-NQPMQGS2.js → chunk-HDCUMOOI.js} +497 -399
- package/dist/chunk-HDCUMOOI.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-PWAHJW4G.js → chunk-OALXJH4Y.js} +86 -33
- package/dist/chunk-OALXJH4Y.js.map +1 -0
- package/dist/{chunk-MI7HBHN3.js → chunk-TC7D3CR3.js} +89 -9
- package/dist/chunk-TC7D3CR3.js.map +1 -0
- 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-UHNYIBXL.js → chunk-UQWSHFVX.js} +1 -1
- package/dist/chunk-UQWSHFVX.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/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
- package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
- package/dist/{file-reference-D06mEEWW.d.ts → file-reference-PRTSLxKx.d.ts} +10 -1
- package/dist/hooks.d.ts +52 -15
- package/dist/hooks.js +12 -22
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.js +82 -18
- 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 +206 -15
- 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 +2 -2
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +114 -3
- package/dist/utils.d.ts +110 -152
- 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 +48 -8
- package/docs/api/interfaces/FileUploadProps.md +46 -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 +6 -6
- 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 +746 -50
- package/docs/api-reference/components.md +26 -12
- 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 +26 -3
- package/docs/implementation-guides/file-upload-storage.md +30 -1
- 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/check-pace-core-compliance.js +512 -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.test.tsx +2 -0
- package/src/components/FileUpload/FileUpload.tsx +10 -1
- package/src/components/Header/Header.test.tsx +47 -18
- package/src/components/Header/Header.tsx +22 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
- package/src/components/PaceAppLayout/README.md +9 -0
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +146 -5
- 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 +5 -0
- package/src/hooks/useFileReference.test.ts +2 -0
- package/src/hooks/useFormDialog.ts +147 -0
- package/src/hooks/usePreventTabReload.ts +106 -0
- package/src/hooks/useSecureDataAccess.ts +2 -2
- 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__/rbac-role-isolation.test.ts +456 -0
- 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/styles/core.css +5 -5
- package/src/types/database.generated.ts +63 -9
- package/src/types/file-reference.ts +9 -0
- package/src/utils/__tests__/timezone.test.ts +345 -0
- package/src/utils/file-reference/__tests__/file-reference.test.ts +60 -4
- package/src/utils/file-reference/index.ts +13 -2
- 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/security/secureDataAccess.ts +1 -1
- package/src/utils/storage/helpers.ts +68 -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-MI7HBHN3.js.map +0 -1
- package/dist/chunk-NQPMQGS2.js.map +0 -1
- package/dist/chunk-PWAHJW4G.js.map +0 -1
- package/dist/chunk-UHNYIBXL.js.map +0 -1
- package/dist/chunk-W22JP75J.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-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file DatePickerWithTimezone Component
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DatePickerWithTimezone
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Date picker component that displays timezone information alongside the calendar.
|
|
8
|
+
* Provides a calendar interface with timezone context for date selection.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Calendar date selection
|
|
12
|
+
* - Timezone display (shows "Local" when matches user timezone)
|
|
13
|
+
* - Optional "Done" button
|
|
14
|
+
* - Accessible date selection
|
|
15
|
+
* - Keyboard navigation support
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { DatePickerWithTimezone } from '@jmruthers/pace-core/components';
|
|
20
|
+
* import { useState } from 'react';
|
|
21
|
+
*
|
|
22
|
+
* function DateSelector() {
|
|
23
|
+
* const [date, setDate] = useState<Date>();
|
|
24
|
+
*
|
|
25
|
+
* return (
|
|
26
|
+
* <DatePickerWithTimezone
|
|
27
|
+
* selected={date}
|
|
28
|
+
* onSelect={setDate}
|
|
29
|
+
* timezone="America/New_York"
|
|
30
|
+
* onDone={() => console.log('Date selected')}
|
|
31
|
+
* />
|
|
32
|
+
* );
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @accessibility
|
|
37
|
+
* - WCAG 2.1 AA compliant
|
|
38
|
+
* - Keyboard navigation support
|
|
39
|
+
* - Screen reader friendly
|
|
40
|
+
* - Focus management
|
|
41
|
+
* - Proper ARIA attributes
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import * as React from 'react';
|
|
45
|
+
import { Calendar } from '../Calendar';
|
|
46
|
+
import { Button } from '../Button';
|
|
47
|
+
import { Clock } from 'lucide-react';
|
|
48
|
+
import { getUserTimeZone } from '../../utils/timezone';
|
|
49
|
+
import { cn } from '../../utils/core/cn';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Props for the DatePickerWithTimezone component
|
|
53
|
+
*/
|
|
54
|
+
export interface DatePickerWithTimezoneProps {
|
|
55
|
+
/**
|
|
56
|
+
* Currently selected date
|
|
57
|
+
*/
|
|
58
|
+
selected?: Date;
|
|
59
|
+
/**
|
|
60
|
+
* Date selection handler
|
|
61
|
+
*/
|
|
62
|
+
onSelect: (date: Date | undefined) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Optional callback when "Done" button is clicked
|
|
65
|
+
*/
|
|
66
|
+
onDone?: () => void;
|
|
67
|
+
/**
|
|
68
|
+
* Timezone to display (defaults to user's timezone)
|
|
69
|
+
*/
|
|
70
|
+
timezone?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Additional CSS classes
|
|
73
|
+
*/
|
|
74
|
+
className?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* DatePickerWithTimezone component
|
|
79
|
+
* Date picker with timezone information display
|
|
80
|
+
*
|
|
81
|
+
* @param props - DatePickerWithTimezone configuration
|
|
82
|
+
* @returns JSX.Element - The rendered date picker with timezone
|
|
83
|
+
*/
|
|
84
|
+
export function DatePickerWithTimezone({
|
|
85
|
+
selected,
|
|
86
|
+
onSelect,
|
|
87
|
+
onDone,
|
|
88
|
+
timezone,
|
|
89
|
+
className
|
|
90
|
+
}: DatePickerWithTimezoneProps) {
|
|
91
|
+
const userTimezone = getUserTimeZone();
|
|
92
|
+
const displayTimezone = timezone || userTimezone;
|
|
93
|
+
const timezoneDisplay = displayTimezone === userTimezone ? 'Local' : displayTimezone;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className={cn('flex flex-col', className)}>
|
|
97
|
+
<div className="p-3">
|
|
98
|
+
<Calendar
|
|
99
|
+
mode="single"
|
|
100
|
+
selected={selected}
|
|
101
|
+
onSelect={onSelect}
|
|
102
|
+
initialFocus
|
|
103
|
+
captionLayout="dropdown-buttons"
|
|
104
|
+
fromYear={1900}
|
|
105
|
+
toYear={2100}
|
|
106
|
+
className="p-0"
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div className="flex items-center justify-between border-t border-border px-3 py-2">
|
|
111
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
112
|
+
<Clock className="h-4 w-4" aria-hidden="true" />
|
|
113
|
+
<span>
|
|
114
|
+
Timezone: <span aria-label={`Timezone ${timezoneDisplay}`}>{timezoneDisplay}</span>
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
{onDone && (
|
|
118
|
+
<Button onClick={onDone} size="sm" className="h-8">
|
|
119
|
+
Done
|
|
120
|
+
</Button>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# DatePickerWithTimezone Component
|
|
2
|
+
|
|
3
|
+
Date picker component that displays timezone information alongside the calendar.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `DatePickerWithTimezone` component provides a calendar interface for date selection with timezone context. It displays the timezone information below the calendar and optionally includes a "Done" button.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Calendar date selection
|
|
12
|
+
- Timezone display (shows "Local" when matches user timezone)
|
|
13
|
+
- Optional "Done" button
|
|
14
|
+
- Accessible date selection
|
|
15
|
+
- Keyboard navigation support
|
|
16
|
+
- Integration with pace-core Calendar component
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Basic Example
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { DatePickerWithTimezone } from '@jmruthers/pace-core/components';
|
|
24
|
+
import { useState } from 'react';
|
|
25
|
+
|
|
26
|
+
function DateSelector() {
|
|
27
|
+
const [date, setDate] = useState<Date>();
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<DatePickerWithTimezone
|
|
31
|
+
selected={date}
|
|
32
|
+
onSelect={setDate}
|
|
33
|
+
timezone="America/New_York"
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### With Done Button
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<DatePickerWithTimezone
|
|
43
|
+
selected={date}
|
|
44
|
+
onSelect={setDate}
|
|
45
|
+
timezone="America/New_York"
|
|
46
|
+
onDone={() => {
|
|
47
|
+
console.log('Date selected:', date);
|
|
48
|
+
// Close dialog or perform other actions
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Using User's Timezone
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// When timezone is not provided, uses user's browser timezone
|
|
57
|
+
<DatePickerWithTimezone
|
|
58
|
+
selected={date}
|
|
59
|
+
onSelect={setDate}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
### Props
|
|
66
|
+
|
|
67
|
+
| Prop | Type | Default | Description |
|
|
68
|
+
|------|------|---------|-------------|
|
|
69
|
+
| `selected` | `Date \| undefined` | - | Currently selected date |
|
|
70
|
+
| `onSelect` | `(date: Date \| undefined) => void` | **required** | Date selection handler |
|
|
71
|
+
| `onDone` | `() => void` | - | Optional callback when "Done" button is clicked |
|
|
72
|
+
| `timezone` | `string` | User's timezone | Timezone to display (IANA timezone string) |
|
|
73
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
74
|
+
|
|
75
|
+
## Behavior
|
|
76
|
+
|
|
77
|
+
### Timezone Display
|
|
78
|
+
|
|
79
|
+
- When `timezone` is not provided: Uses the user's browser timezone
|
|
80
|
+
- When `timezone` matches user's timezone: Shows "Local"
|
|
81
|
+
- When `timezone` is different: Shows the timezone name (e.g., "America/New_York")
|
|
82
|
+
|
|
83
|
+
### Date Selection
|
|
84
|
+
|
|
85
|
+
The component uses the pace-core `Calendar` component for date selection. Selected dates are passed directly to `onSelect` without timezone conversion (dates are timezone-agnostic).
|
|
86
|
+
|
|
87
|
+
### Done Button
|
|
88
|
+
|
|
89
|
+
When `onDone` is provided, a "Done" button appears in the footer. Clicking it calls the `onDone` callback without affecting the selected date.
|
|
90
|
+
|
|
91
|
+
## Accessibility
|
|
92
|
+
|
|
93
|
+
- WCAG 2.1 AA compliant
|
|
94
|
+
- Keyboard navigation support (via Calendar component)
|
|
95
|
+
- Screen reader friendly
|
|
96
|
+
- Proper ARIA labels for timezone information
|
|
97
|
+
- Focus management
|
|
98
|
+
|
|
99
|
+
## Integration
|
|
100
|
+
|
|
101
|
+
### With Dialog
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { Dialog, DatePickerWithTimezone } from '@jmruthers/pace-core/components';
|
|
105
|
+
|
|
106
|
+
function DatePickerDialog() {
|
|
107
|
+
const [date, setDate] = useState<Date>();
|
|
108
|
+
const [open, setOpen] = useState(false);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
112
|
+
<DialogContent>
|
|
113
|
+
<DatePickerWithTimezone
|
|
114
|
+
selected={date}
|
|
115
|
+
onSelect={setDate}
|
|
116
|
+
onDone={() => setOpen(false)}
|
|
117
|
+
/>
|
|
118
|
+
</DialogContent>
|
|
119
|
+
</Dialog>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Edge Cases
|
|
125
|
+
|
|
126
|
+
- **Undefined selected date**: Calendar displays with no date selected
|
|
127
|
+
- **Null selected date**: Treated as undefined
|
|
128
|
+
|
|
129
|
+
## Dependencies
|
|
130
|
+
|
|
131
|
+
- `@jmruthers/pace-core/components/Calendar` - Calendar component
|
|
132
|
+
- `@jmruthers/pace-core/components/Button` - Button component
|
|
133
|
+
- `lucide-react` - Clock icon
|
|
134
|
+
- `@jmruthers/pace-core/utils/timezone` - Timezone utilities
|
|
135
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file DatePickerWithTimezone Component Exports
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DatePickerWithTimezone
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { DatePickerWithTimezone } from './DatePickerWithTimezone';
|
|
9
|
+
export type { DatePickerWithTimezoneProps } from './DatePickerWithTimezone';
|
|
10
|
+
|
|
@@ -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
|
+
|