@jmruthers/pace-core 0.5.68 → 0.5.69
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/dist/{DataTable-4IUY7BXB.js → DataTable-MPBSXUC6.js} +5 -6
- package/dist/{PublicLoadingSpinner-DdKXTkCZ.d.ts → PublicLoadingSpinner-BOdyU3u-.d.ts} +1 -1
- package/dist/{chunk-PXWEDX7Y.js → chunk-2ARQW6VX.js} +3 -3
- package/dist/{chunk-MOJXHWDE.js → chunk-6JILXFEA.js} +335 -5
- package/dist/chunk-6JILXFEA.js.map +1 -0
- package/dist/{chunk-D7ARGIA3.js → chunk-6RBH67W7.js} +23 -6
- package/dist/chunk-6RBH67W7.js.map +1 -0
- package/dist/{chunk-ZMS23NS5.js → chunk-FJTAWPAQ.js} +3 -5
- package/dist/{chunk-ZMS23NS5.js.map → chunk-FJTAWPAQ.js.map} +1 -1
- package/dist/{chunk-OPCWH3A4.js → chunk-NO5QHMDX.js} +7 -6
- package/dist/chunk-NO5QHMDX.js.map +1 -0
- package/dist/{chunk-ZPK5656W.js → chunk-O3NWNXDY.js} +4 -5
- package/dist/chunk-O3NWNXDY.js.map +1 -0
- package/dist/{chunk-UYA6U6H7.js → chunk-Q2UP3ZWQ.js} +4 -4
- package/dist/{chunk-KRCRNXPD.js → chunk-RVYGJPOD.js} +79 -18
- package/dist/chunk-RVYGJPOD.js.map +1 -0
- package/dist/{chunk-NN45OBIS.js → chunk-UCMHBF7Y.js} +3 -5
- package/dist/{chunk-NN45OBIS.js.map → chunk-UCMHBF7Y.js.map} +1 -1
- package/dist/{chunk-ZPG4XPV5.js → chunk-V3QO3LL7.js} +5 -7
- package/dist/chunk-V3QO3LL7.js.map +1 -0
- package/dist/{chunk-U6GPOF6J.js → chunk-ZXJGZLLO.js} +17 -17
- package/dist/{chunk-U6GPOF6J.js.map → chunk-ZXJGZLLO.js.map} +1 -1
- package/dist/components.d.ts +1 -1
- package/dist/components.js +8 -9
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +9 -6
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +16 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.js +5 -7
- package/dist/rbac/index.js +5 -6
- package/dist/{usePublicRouteParams-CdoFxnJK.d.ts → usePublicRouteParams-Ua1Vz-HG.d.ts} +35 -1
- package/dist/utils.d.ts +4 -1
- package/dist/utils.js +3 -3
- package/docs/DOCUMENTATION_CHECKLIST.md +281 -0
- package/docs/README.md +22 -10
- 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/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- 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/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/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.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/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 +2 -2
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACContextType.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACProviderProps.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.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/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/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/UsePublicEventLogoOptions.md +1 -1
- 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/UsePublicRouteParamsReturn.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 +39 -14
- package/docs/architecture/services.md +374 -0
- package/docs/best-practices/README.md +1 -1
- package/docs/best-practices/testing.md +1 -1
- package/docs/breaking-changes.md +182 -0
- package/docs/common-patterns.md +445 -0
- package/docs/core-concepts/authentication.md +26 -11
- package/docs/core-concepts/events.md +2 -0
- package/docs/core-concepts/organisations.md +2 -0
- package/docs/core-concepts/permissions.md +2 -0
- package/docs/{INDEX.md → documentation-index.md} +26 -38
- package/docs/faq.md +286 -0
- package/docs/{FILE_REFERENCE_SYSTEM.md → file-reference-system.md} +1 -1
- package/docs/getting-started/installation-guide.md +284 -0
- package/docs/getting-started/quick-start.md +8 -1
- package/docs/implementation-guides/app-layout.md +3 -1
- package/docs/implementation-guides/data-tables.md +2 -0
- package/docs/implementation-guides/dynamic-colors.md +47 -2
- package/docs/implementation-guides/event-theming-summary.md +220 -0
- package/docs/implementation-guides/forms.md +9 -7
- package/docs/implementation-guides/navigation.md +2 -0
- package/docs/migration/service-architecture.md +351 -0
- package/docs/rbac/README-rbac-rls-integration.md +2 -2
- package/docs/rbac/README.md +1 -1
- package/docs/rbac/examples/rbac-rls-integration-example.md +3 -3
- package/docs/rbac/quick-start.md +2 -0
- package/docs/rbac/rbac-rls-integration.md +2 -2
- package/docs/style-guide.md +136 -1
- package/docs/testing/README.md +1 -1
- package/docs/troubleshooting/authentication-issues.md +334 -0
- package/docs/troubleshooting/common-issues.md +2 -0
- package/docs/troubleshooting/styling-issues.md +199 -144
- package/docs/usage.md +23 -2
- package/package.json +1 -1
- package/src/__tests__/{TESTING_GUIDELINES.md → TEST_GUIDE_CURSOR.md} +20 -0
- package/src/__tests__/TEST_GUIDE_HUMAN.md +103 -0
- package/src/__tests__/fixtures/test-data.ts +90 -0
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +260 -0
- package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +224 -0
- package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +273 -0
- package/src/__tests__/helpers/__tests__/test-providers.test.tsx +98 -0
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +436 -0
- package/src/__tests__/helpers/__tests__/timer-utils.test.ts +371 -0
- package/src/__tests__/helpers/component-test-utils.tsx +14 -4
- package/src/__tests__/helpers/optimized-test-setup.ts +68 -0
- package/src/__tests__/helpers/test-providers.tsx +329 -0
- package/src/__tests__/helpers/test-utils.tsx +91 -45
- package/src/__tests__/helpers/timer-utils.ts +71 -0
- package/src/__tests__/hooks/usePermissions.test.ts +1 -5
- package/src/__tests__/integration/UserProfile.test.tsx +1 -5
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +42 -12
- package/src/__tests__/setup.ts +34 -28
- package/src/components/Alert/Alert.test.tsx +1 -5
- package/src/components/Avatar/Avatar.test.tsx +1 -5
- package/src/components/Button/Button.test.tsx +4 -20
- package/src/components/Card/Card.test.tsx +1 -5
- package/src/components/Checkbox/Checkbox.test.tsx +1 -5
- package/src/components/DataTable/__tests__/DataTable.comprehensive.test.tsx +1 -5
- package/src/components/DataTable/__tests__/DataTable.test.tsx +45 -49
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +1 -5
- package/src/components/DataTable/__tests__/styles.test.ts +382 -0
- package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +409 -0
- package/src/components/DataTable/core/__tests__/ActionManager.test.ts +634 -0
- package/src/components/DataTable/core/__tests__/DataManager.test.ts +519 -0
- package/src/components/DataTable/core/__tests__/StateManager.test.ts +714 -0
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +592 -0
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +354 -0
- package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +539 -0
- package/src/components/Dialog/examples/__tests__/SmartDialogExample.unit.test.tsx +1 -5
- package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +1 -8
- package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +34 -38
- package/src/components/Footer/Footer.test.tsx +1 -5
- package/src/components/Form/Form.test.tsx +22 -35
- package/src/components/Header/Header.test.tsx +1 -9
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +1 -5
- package/src/components/Input/Input.test.tsx +2 -10
- package/src/components/LoginForm/LoginForm.test.tsx +1 -5
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +24 -24
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +1 -6
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +6 -16
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +1 -5
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.rbac.test.tsx +1 -5
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +1 -7
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +1 -9
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +1 -9
- package/src/components/PublicLayout/PublicErrorBoundary.tsx +4 -5
- package/src/components/PublicLayout/PublicPageHeader.tsx +13 -9
- package/src/components/PublicLayout/__tests__/EventLogo.test.tsx +666 -0
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +457 -0
- package/src/components/PublicLayout/__tests__/PublicLoadingSpinner.test.tsx +393 -0
- package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +351 -0
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +374 -0
- package/src/components/PublicLayout/__tests__/PublicPageLayout.test.tsx +388 -0
- package/src/components/Select/Select.bug-test.tsx +69 -0
- package/src/components/Select/Select.refactored.tsx +497 -0
- package/src/components/Select/Select.test.tsx +42 -49
- package/src/components/Select/Select.tsx +5 -2
- package/src/components/Select/hooks.ts +254 -0
- package/src/components/Switch/Switch.test.tsx +1 -5
- package/src/components/Table/__tests__/Table.test.tsx +775 -0
- package/src/components/Toast/Toast.test.tsx +15 -8
- package/src/components/Tooltip/Tooltip.test.tsx +1 -5
- package/src/components/UserMenu/UserMenu.test.tsx +3 -15
- package/src/components/__tests__/FileDisplay.test.tsx +575 -0
- package/src/components/__tests__/FileUpload.test.tsx +446 -0
- package/src/components/__tests__/SuperAdminGuard.test.tsx +422 -354
- package/src/hooks/__tests__/ServiceHooks.test.tsx +613 -0
- package/src/hooks/__tests__/hooks.integration.test.tsx +1 -10
- package/src/hooks/__tests__/useApiFetch.unit.test.ts +10 -14
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +307 -0
- package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +1 -6
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +1 -5
- package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +6 -9
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +321 -0
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +583 -0
- package/src/hooks/__tests__/usePublicEventLogo.unit.test.ts +640 -0
- package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +435 -0
- package/src/hooks/__tests__/useRBAC.unit.test.ts +10 -10
- package/src/hooks/__tests__/useStorage.unit.test.ts +751 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/public/usePublicEvent.ts +30 -9
- package/src/hooks/public/usePublicRouteParams.ts +13 -3
- package/src/hooks/services/useAuth.ts +50 -0
- package/src/hooks/services/useAuthService.ts +30 -0
- package/src/hooks/services/useCurrentEvent.ts +36 -0
- package/src/hooks/services/useCurrentOrganisation.ts +52 -0
- package/src/hooks/services/useEventService.ts +30 -0
- package/src/hooks/services/useInactivityService.ts +30 -0
- package/src/hooks/services/useOrganisationService.ts +30 -0
- package/src/hooks/services/usePermissions.ts +70 -0
- package/src/hooks/services/useRBACService.ts +30 -0
- package/src/hooks/useCounter.test.ts +1 -5
- package/src/hooks/useEventTheme.ts +86 -0
- package/src/hooks/useOrganisationPermissions.test.ts +2 -5
- package/src/hooks/useOrganisationSecurity.test.ts +1 -5
- package/src/hooks/usePermissionCache.test.ts +1 -5
- package/src/hooks/usePermissionCheck.ts +150 -0
- package/src/hooks/useSecureDataAccess.test.ts +1 -5
- package/src/index.ts +1 -0
- package/src/providers/OrganisationProvider.test.tsx +1 -5
- package/src/providers/OrganisationProvider.tsx +56 -4
- package/src/providers/UnifiedAuthProvider.test.tsx +1 -5
- package/src/providers/__tests__/AuthProvider.test.tsx +105 -439
- package/src/providers/__tests__/AuthProvider.test.tsx.backup +771 -0
- package/src/providers/__tests__/EventProvider.test.tsx +211 -110
- package/src/providers/__tests__/EventProvider.test.tsx.backup +824 -0
- package/src/providers/__tests__/InactivityProvider.test.tsx +1 -5
- package/src/providers/__tests__/OrganisationProvider.test.tsx +97 -261
- package/src/providers/__tests__/OrganisationProvider.test.tsx.backup +820 -0
- package/src/providers/__tests__/ServiceProviders.test.tsx +477 -0
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +72 -504
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup +911 -0
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup2 +166 -0
- package/src/providers/services/AuthServiceProvider.tsx +65 -0
- package/src/providers/services/EventServiceProvider.tsx +83 -0
- package/src/providers/services/InactivityServiceProvider.tsx +83 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +77 -0
- package/src/providers/services/RBACServiceProvider.tsx +79 -0
- package/src/providers/services/UnifiedAuthProvider.tsx +368 -0
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +210 -0
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +269 -0
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +892 -0
- package/src/rbac/__tests__/engine.comprehensive.test.ts +954 -0
- package/src/rbac/__tests__/integration.authflow.test.tsx +1 -5
- package/src/rbac/__tests__/integration.navigation.test.tsx +1 -4
- package/src/rbac/__tests__/rbac-core.test.tsx +2 -7
- package/src/rbac/__tests__/rbac-functions.test.ts +1 -9
- package/src/rbac/__tests__/rbac-integration.test.ts +1 -9
- package/src/rbac/api.test.ts +1 -9
- package/src/rbac/cache.test.ts +10 -8
- package/src/rbac/cli/__tests__/policy-manager.test.ts +339 -0
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -5
- package/src/rbac/components/NavigationProvider.test.tsx +1 -5
- package/src/rbac/components/PagePermissionProvider.test.tsx +1 -5
- package/src/rbac/components/SecureDataProvider.test.tsx +1 -5
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +25 -29
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +27 -30
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +23 -27
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +18 -22
- package/src/rbac/config.test.ts +1 -5
- package/src/rbac/hooks/useCan.test.ts +262 -9
- package/src/rbac/hooks/usePermissions.test.ts +246 -6
- package/src/rbac/hooks/useRBAC.simple.test.ts +1 -5
- package/src/rbac/hooks/useRBAC.test.ts +472 -198
- package/src/rbac/providers/__tests__/RBACProvider.test.tsx +1 -9
- package/src/services/AuthService.ts +416 -0
- package/src/services/EventService.ts +366 -0
- package/src/services/InactivityService.ts +388 -0
- package/src/services/OrganisationService.ts +592 -0
- package/src/services/RBACService.ts +522 -0
- package/src/services/__tests__/AuthService.test.ts +356 -0
- package/src/services/__tests__/BaseService.test.ts +314 -0
- package/src/services/__tests__/EventService.test.ts +489 -0
- package/src/services/__tests__/InactivityService.test.ts +403 -0
- package/src/services/__tests__/OrganisationService.test.ts +660 -0
- package/src/services/__tests__/RBACService.test.ts +492 -0
- package/src/services/base/BaseService.ts +87 -0
- package/src/services/interfaces/IAuthService.ts +39 -0
- package/src/services/interfaces/IEventService.ts +30 -0
- package/src/services/interfaces/IInactivityService.ts +31 -0
- package/src/services/interfaces/IOrganisationService.ts +41 -0
- package/src/services/interfaces/IRBACService.ts +62 -0
- package/src/theming/__tests__/runtime.test.ts +540 -0
- package/src/types/__tests__/file-reference.test.ts +447 -0
- package/src/types/__tests__/organisation.test.ts +1133 -0
- package/src/types/__tests__/theme.test.ts +830 -0
- package/src/types/__tests__/type-validation.test.ts +527 -0
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +1 -5
- package/src/utils/__tests__/debugLogger.test.ts +417 -0
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +1 -6
- package/src/utils/__tests__/dynamicUtils.unit.test.ts +1 -5
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +35 -35
- package/src/utils/__tests__/organisationContext.unit.test.ts +1 -5
- package/src/utils/__tests__/performanceBudgets.unit.test.ts +5 -11
- package/src/utils/__tests__/secureErrors.unit.test.ts +1 -6
- package/src/utils/__tests__/secureStorage.unit.test.ts +1 -5
- package/src/utils/__tests__/securityMonitor.unit.test.ts +1 -5
- package/src/utils/__tests__/sessionTracking.unit.test.ts +1 -5
- package/src/utils/appIdResolver.test.ts +6 -10
- package/src/utils/appNameResolver.simple.test.ts +142 -0
- package/src/utils/appNameResolver.test.ts +31 -458
- package/src/utils/appNameResolver.test.ts.backup +494 -0
- package/src/utils/debugLogger.ts +26 -5
- package/src/utils/formatDate.test.ts +1 -5
- package/src/utils/organisationContext.test.ts +1 -5
- package/src/utils/performanceBudgets.ts +3 -4
- package/src/utils/secureDataAccess.test.ts +1 -5
- package/src/utils/storage/__tests__/helpers.unit.test.ts +1 -5
- package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +1 -5
- package/dist/chunk-D7ARGIA3.js.map +0 -1
- package/dist/chunk-IPCH4YPT.js +0 -315
- package/dist/chunk-IPCH4YPT.js.map +0 -1
- package/dist/chunk-KRCRNXPD.js.map +0 -1
- package/dist/chunk-MOJXHWDE.js.map +0 -1
- package/dist/chunk-OPCWH3A4.js.map +0 -1
- package/dist/chunk-ZPG4XPV5.js.map +0 -1
- package/dist/chunk-ZPK5656W.js.map +0 -1
- package/docs/getting-started/installation.md +0 -269
- package/src/__tests__/REBUILD_PLAN.md +0 -223
- /package/dist/{DataTable-4IUY7BXB.js.map → DataTable-MPBSXUC6.js.map} +0 -0
- /package/dist/{chunk-PXWEDX7Y.js.map → chunk-2ARQW6VX.js.map} +0 -0
- /package/dist/{chunk-UYA6U6H7.js.map → chunk-Q2UP3ZWQ.js.map} +0 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Select Component - Refactored SOLID Implementation
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/Select
|
|
5
|
+
* @since 0.4.0
|
|
6
|
+
*
|
|
7
|
+
* Refactored Select component following SOLID principles:
|
|
8
|
+
* - Single Responsibility: Each component has one clear purpose
|
|
9
|
+
* - Open/Closed: Easy to extend without modification
|
|
10
|
+
* - Liskov Substitution: Components can be substituted
|
|
11
|
+
* - Interface Segregation: Small, focused interfaces
|
|
12
|
+
* - Dependency Inversion: Depends on abstractions, not concretions
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as React from "react";
|
|
16
|
+
import { Search, X, ChevronDown, Check } from "lucide-react";
|
|
17
|
+
import { Button, type ButtonProps } from "../Button/Button";
|
|
18
|
+
import { cn } from "../../utils/cn";
|
|
19
|
+
import {
|
|
20
|
+
useSelectState,
|
|
21
|
+
useSelectEvents,
|
|
22
|
+
useSelectSearch,
|
|
23
|
+
type SelectState,
|
|
24
|
+
type SelectActions,
|
|
25
|
+
type UseSelectStateProps
|
|
26
|
+
} from "./hooks";
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// TYPES AND INTERFACES
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
export interface SelectContextValue extends SelectState {
|
|
33
|
+
actions: SelectActions;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SelectProps extends Omit<React.HTMLAttributes<HTMLFormElement>, 'onChange' | 'onKeyDown' | 'onFocus' | 'onBlur'> {
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SelectTriggerProps extends Omit<ButtonProps, 'onClick' | 'onKeyDown'> {
|
|
42
|
+
children: React.ReactNode;
|
|
43
|
+
asChild?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SelectValueProps {
|
|
47
|
+
placeholder?: string;
|
|
48
|
+
children?: React.ReactNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SelectContentProps {
|
|
52
|
+
children: React.ReactNode;
|
|
53
|
+
className?: string;
|
|
54
|
+
searchable?: boolean;
|
|
55
|
+
searchPlaceholder?: string;
|
|
56
|
+
maxHeight?: string;
|
|
57
|
+
style?: React.CSSProperties;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SelectItemProps {
|
|
61
|
+
value: string;
|
|
62
|
+
children: React.ReactNode;
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
className?: string;
|
|
65
|
+
onClick?: (e: React.MouseEvent) => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// CONTEXT
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
const SelectContext = React.createContext<SelectContextValue | null>(null);
|
|
73
|
+
|
|
74
|
+
const useSelectContext = () => {
|
|
75
|
+
const context = React.useContext(SelectContext);
|
|
76
|
+
if (!context) {
|
|
77
|
+
throw new Error('Select components must be used within a Select');
|
|
78
|
+
}
|
|
79
|
+
return context;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// ROOT COMPONENT
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
export const Select = React.forwardRef<HTMLFormElement, SelectProps & UseSelectStateProps>(
|
|
87
|
+
({
|
|
88
|
+
children,
|
|
89
|
+
className,
|
|
90
|
+
...selectProps
|
|
91
|
+
}, ref) => {
|
|
92
|
+
const internalRef = React.useRef<HTMLFormElement>(null);
|
|
93
|
+
const selectRef = React.useMemo(() => {
|
|
94
|
+
if (ref && typeof ref === 'object' && 'current' in ref) {
|
|
95
|
+
return ref as React.RefObject<HTMLFormElement>;
|
|
96
|
+
}
|
|
97
|
+
return internalRef;
|
|
98
|
+
}, [ref]);
|
|
99
|
+
|
|
100
|
+
// Use custom hooks for state management
|
|
101
|
+
const { state, actions } = useSelectState(selectProps);
|
|
102
|
+
const { isSelecting } = useSelectEvents({ state, actions, selectRef });
|
|
103
|
+
|
|
104
|
+
// Find selected text when value changes
|
|
105
|
+
React.useEffect(() => {
|
|
106
|
+
if (state.value && !state.selectedText) {
|
|
107
|
+
// Find the SelectItem with the matching value and extract its text
|
|
108
|
+
const selectElement = selectRef.current;
|
|
109
|
+
if (selectElement) {
|
|
110
|
+
const selectItem = selectElement.querySelector(`[data-value="${state.value}"]`);
|
|
111
|
+
if (selectItem) {
|
|
112
|
+
const textContent = selectItem.textContent?.trim() || '';
|
|
113
|
+
if (textContent) {
|
|
114
|
+
actions.setSelectedText(textContent);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, [state.value, state.selectedText, actions, selectRef]);
|
|
120
|
+
|
|
121
|
+
const contextValue = React.useMemo<SelectContextValue>(() => ({
|
|
122
|
+
...state,
|
|
123
|
+
actions,
|
|
124
|
+
}), [state, actions]);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<form
|
|
128
|
+
ref={selectRef}
|
|
129
|
+
className={cn("relative", className)}
|
|
130
|
+
data-value={state.value}
|
|
131
|
+
data-testid="select-root"
|
|
132
|
+
>
|
|
133
|
+
<SelectContext.Provider value={contextValue}>
|
|
134
|
+
{children}
|
|
135
|
+
</SelectContext.Provider>
|
|
136
|
+
</form>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
Select.displayName = "Select";
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// TRIGGER COMPONENT
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerProps>(
|
|
147
|
+
({ children, className, variant = "outline", size = "default", asChild = false, ...props }, ref) => {
|
|
148
|
+
const { open, disabled, value, actions } = useSelectContext();
|
|
149
|
+
|
|
150
|
+
const handleClick = () => {
|
|
151
|
+
actions.setOpen(!open);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
155
|
+
if (disabled) return;
|
|
156
|
+
|
|
157
|
+
switch (e.key) {
|
|
158
|
+
case 'Enter':
|
|
159
|
+
case ' ':
|
|
160
|
+
case 'ArrowDown':
|
|
161
|
+
case 'ArrowUp':
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
actions.setOpen(true);
|
|
164
|
+
break;
|
|
165
|
+
case 'Escape':
|
|
166
|
+
if (open) {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
actions.setOpen(false);
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const triggerProps = {
|
|
175
|
+
ref,
|
|
176
|
+
type: "button" as const,
|
|
177
|
+
role: "combobox",
|
|
178
|
+
"aria-expanded": open,
|
|
179
|
+
"aria-haspopup": "listbox",
|
|
180
|
+
disabled,
|
|
181
|
+
className: cn(
|
|
182
|
+
"!justify-between relative w-full",
|
|
183
|
+
"[&_svg]:pointer-events-none",
|
|
184
|
+
open && "!rounded-b-none !border-b-0",
|
|
185
|
+
className
|
|
186
|
+
),
|
|
187
|
+
style: {
|
|
188
|
+
...props.style,
|
|
189
|
+
overflow: 'hidden',
|
|
190
|
+
textOverflow: 'ellipsis',
|
|
191
|
+
whiteSpace: 'nowrap'
|
|
192
|
+
},
|
|
193
|
+
onClick: handleClick,
|
|
194
|
+
onKeyDown: handleKeyDown,
|
|
195
|
+
"data-testid": "select-trigger",
|
|
196
|
+
"data-value": value,
|
|
197
|
+
...props
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (asChild) {
|
|
201
|
+
const childChildren = React.Children.toArray((children as React.ReactElement).props.children);
|
|
202
|
+
const hasChevron = childChildren.some(child =>
|
|
203
|
+
React.isValidElement(child) &&
|
|
204
|
+
(child.type === ChevronDown ||
|
|
205
|
+
(child.type === 'svg' && child.props['data-testid'] === 'chevron-down') ||
|
|
206
|
+
(typeof child === 'object' && 'type' in child && typeof child.type === 'function' && child.type.name === 'ChevronDown'))
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return React.cloneElement(children as React.ReactElement, {
|
|
210
|
+
...triggerProps,
|
|
211
|
+
children: hasChevron ? childChildren : [
|
|
212
|
+
...childChildren,
|
|
213
|
+
<ChevronDown
|
|
214
|
+
key="chevron-down"
|
|
215
|
+
className={cn(
|
|
216
|
+
"h-4 w-4 opacity-50 transition-transform pointer-events-none float-right",
|
|
217
|
+
open && "rotate-180"
|
|
218
|
+
)}
|
|
219
|
+
/>
|
|
220
|
+
]
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<Button
|
|
226
|
+
ref={ref}
|
|
227
|
+
type="button"
|
|
228
|
+
role="combobox"
|
|
229
|
+
aria-expanded={open}
|
|
230
|
+
aria-haspopup="listbox"
|
|
231
|
+
disabled={disabled}
|
|
232
|
+
variant={variant}
|
|
233
|
+
size={size}
|
|
234
|
+
className={cn(
|
|
235
|
+
"!justify-between relative w-full",
|
|
236
|
+
"[&_svg]:pointer-events-none",
|
|
237
|
+
open && "!rounded-b-none !border-b-0",
|
|
238
|
+
className
|
|
239
|
+
)}
|
|
240
|
+
style={{
|
|
241
|
+
...props.style,
|
|
242
|
+
overflow: 'hidden',
|
|
243
|
+
textOverflow: 'ellipsis',
|
|
244
|
+
whiteSpace: 'nowrap'
|
|
245
|
+
}}
|
|
246
|
+
onClick={handleClick}
|
|
247
|
+
onKeyDown={handleKeyDown}
|
|
248
|
+
data-testid="select-trigger"
|
|
249
|
+
data-value={value}
|
|
250
|
+
{...props}
|
|
251
|
+
>
|
|
252
|
+
{children}
|
|
253
|
+
<ChevronDown
|
|
254
|
+
className={cn(
|
|
255
|
+
"h-4 w-4 opacity-50 transition-transform pointer-events-none float-right",
|
|
256
|
+
open && "rotate-180"
|
|
257
|
+
)}
|
|
258
|
+
/>
|
|
259
|
+
</Button>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
SelectTrigger.displayName = "SelectTrigger";
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// VALUE COMPONENT
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
export const SelectValue = React.forwardRef<HTMLSpanElement, SelectValueProps>(
|
|
270
|
+
({ placeholder = "Select an option...", children }, ref) => {
|
|
271
|
+
const { selectedText } = useSelectContext();
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<span ref={ref} data-testid="select-value">
|
|
275
|
+
{children || (selectedText ? selectedText : placeholder)}
|
|
276
|
+
</span>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
SelectValue.displayName = "SelectValue";
|
|
281
|
+
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// CONTENT COMPONENT
|
|
284
|
+
// ============================================================================
|
|
285
|
+
|
|
286
|
+
export const SelectContent = React.forwardRef<HTMLUListElement, SelectContentProps>(
|
|
287
|
+
({
|
|
288
|
+
children,
|
|
289
|
+
className,
|
|
290
|
+
searchable = false,
|
|
291
|
+
searchPlaceholder = "Search...",
|
|
292
|
+
maxHeight = "20rem",
|
|
293
|
+
style
|
|
294
|
+
}, ref) => {
|
|
295
|
+
const { open, actions } = useSelectContext();
|
|
296
|
+
const { searchTerm, setSearchTerm, filteredChildren, searchInputRef } = useSelectSearch({
|
|
297
|
+
children,
|
|
298
|
+
searchable,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Focus search input when dropdown opens
|
|
302
|
+
React.useEffect(() => {
|
|
303
|
+
if (open && searchable && searchInputRef.current) {
|
|
304
|
+
searchInputRef.current.focus();
|
|
305
|
+
}
|
|
306
|
+
}, [open, searchable]);
|
|
307
|
+
|
|
308
|
+
if (!open) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<ul
|
|
314
|
+
ref={ref}
|
|
315
|
+
className={cn(
|
|
316
|
+
"absolute z-[99999] w-full overflow-y-auto rounded-b-md border border-t-0 border-main-300 bg-main-50 shadow-lg",
|
|
317
|
+
"list-none p-0 m-0",
|
|
318
|
+
className
|
|
319
|
+
)}
|
|
320
|
+
style={{
|
|
321
|
+
top: '100%',
|
|
322
|
+
left: 0,
|
|
323
|
+
right: 0,
|
|
324
|
+
maxHeight,
|
|
325
|
+
position: 'absolute',
|
|
326
|
+
zIndex: 99999,
|
|
327
|
+
...style
|
|
328
|
+
}}
|
|
329
|
+
data-testid="select-content"
|
|
330
|
+
role="listbox"
|
|
331
|
+
>
|
|
332
|
+
{searchable && (
|
|
333
|
+
<div className="p-2 border-b border-main-200">
|
|
334
|
+
<div className="relative">
|
|
335
|
+
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-main-400" />
|
|
336
|
+
<input
|
|
337
|
+
ref={searchInputRef}
|
|
338
|
+
type="text"
|
|
339
|
+
placeholder={searchPlaceholder}
|
|
340
|
+
value={searchTerm}
|
|
341
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
342
|
+
onKeyDown={(e) => {
|
|
343
|
+
if (e.key === 'Escape') {
|
|
344
|
+
e.preventDefault();
|
|
345
|
+
setSearchTerm('');
|
|
346
|
+
}
|
|
347
|
+
}}
|
|
348
|
+
className="w-full pl-8 pr-8 py-1 text-sm border border-main-200 rounded focus:outline-none focus:ring-2 focus:ring-main-500"
|
|
349
|
+
data-testid="select-search-input"
|
|
350
|
+
aria-label="Search options"
|
|
351
|
+
/>
|
|
352
|
+
{searchTerm && (
|
|
353
|
+
<button
|
|
354
|
+
type="button"
|
|
355
|
+
onClick={() => setSearchTerm('')}
|
|
356
|
+
className="absolute right-2 top-1/2 transform -translate-y-1/2 text-main-400 hover:text-main-600"
|
|
357
|
+
data-testid="select-clear-search"
|
|
358
|
+
aria-label="Clear search"
|
|
359
|
+
>
|
|
360
|
+
<X className="h-4 w-4" />
|
|
361
|
+
</button>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
)}
|
|
366
|
+
{filteredChildren}
|
|
367
|
+
</ul>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
SelectContent.displayName = "SelectContent";
|
|
372
|
+
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// ITEM COMPONENT
|
|
375
|
+
// ============================================================================
|
|
376
|
+
|
|
377
|
+
export const SelectItem = React.forwardRef<HTMLLIElement, SelectItemProps>(
|
|
378
|
+
({ value, children, disabled = false, className, onClick }, ref) => {
|
|
379
|
+
const { value: selectedValue, actions } = useSelectContext();
|
|
380
|
+
const isSelected = selectedValue === value;
|
|
381
|
+
|
|
382
|
+
// Extract text content from children for display
|
|
383
|
+
const getTextContent = (children: React.ReactNode): string => {
|
|
384
|
+
if (typeof children === 'string') return children;
|
|
385
|
+
if (typeof children === 'number') return children.toString();
|
|
386
|
+
if (React.isValidElement(children) && children.props.children) {
|
|
387
|
+
return getTextContent(children.props.children);
|
|
388
|
+
}
|
|
389
|
+
if (Array.isArray(children)) {
|
|
390
|
+
return children.map(getTextContent).join('');
|
|
391
|
+
}
|
|
392
|
+
return '';
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const itemText = getTextContent(children);
|
|
396
|
+
|
|
397
|
+
const handleMouseDown = (e: React.MouseEvent) => {
|
|
398
|
+
if (!disabled) {
|
|
399
|
+
const event = new CustomEvent('selectItemMouseDown', { detail: { value } });
|
|
400
|
+
document.dispatchEvent(event);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
405
|
+
if (!disabled) {
|
|
406
|
+
if (onClick) {
|
|
407
|
+
onClick(e);
|
|
408
|
+
}
|
|
409
|
+
actions.setValue(value, itemText);
|
|
410
|
+
actions.setOpen(false);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
415
|
+
if (disabled) return;
|
|
416
|
+
|
|
417
|
+
switch (e.key) {
|
|
418
|
+
case 'Enter':
|
|
419
|
+
case ' ':
|
|
420
|
+
e.preventDefault();
|
|
421
|
+
if (onClick) {
|
|
422
|
+
onClick(e as any);
|
|
423
|
+
}
|
|
424
|
+
actions.setValue(value, itemText);
|
|
425
|
+
actions.setOpen(false);
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<li
|
|
432
|
+
ref={ref}
|
|
433
|
+
data-value={value}
|
|
434
|
+
className={cn(
|
|
435
|
+
"relative flex cursor-pointer select-none items-start rounded-sm px-2 py-1.5 text-sm outline-none",
|
|
436
|
+
"hover:bg-main-100 focus:bg-main-100",
|
|
437
|
+
"break-words min-w-0",
|
|
438
|
+
isSelected && "bg-main-100 text-main-900",
|
|
439
|
+
disabled && "pointer-events-none opacity-50",
|
|
440
|
+
className
|
|
441
|
+
)}
|
|
442
|
+
onMouseDown={handleMouseDown}
|
|
443
|
+
onClick={handleClick}
|
|
444
|
+
onKeyDown={handleKeyDown}
|
|
445
|
+
data-testid="select-item"
|
|
446
|
+
data-disabled={disabled ? "true" : undefined}
|
|
447
|
+
role="option"
|
|
448
|
+
aria-selected={isSelected}
|
|
449
|
+
>
|
|
450
|
+
{children}
|
|
451
|
+
{isSelected && (
|
|
452
|
+
<Check className="absolute right-2 h-4 w-4 flex-shrink-0 mt-0.5" />
|
|
453
|
+
)}
|
|
454
|
+
</li>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
SelectItem.displayName = "SelectItem";
|
|
459
|
+
|
|
460
|
+
// ============================================================================
|
|
461
|
+
// ADDITIONAL COMPONENTS (for backward compatibility)
|
|
462
|
+
// ============================================================================
|
|
463
|
+
|
|
464
|
+
export const SelectGroup = React.forwardRef<HTMLDivElement, { children: React.ReactNode; className?: string }>(
|
|
465
|
+
({ children, className }, ref) => {
|
|
466
|
+
return (
|
|
467
|
+
<div ref={ref} className={cn("p-1", className)} data-testid="select-group">
|
|
468
|
+
{children}
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
SelectGroup.displayName = "SelectGroup";
|
|
474
|
+
|
|
475
|
+
export const SelectLabel = React.forwardRef<HTMLDivElement, { children: React.ReactNode; className?: string }>(
|
|
476
|
+
({ children, className }, ref) => {
|
|
477
|
+
return (
|
|
478
|
+
<div ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} data-testid="select-label">
|
|
479
|
+
{children}
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
);
|
|
484
|
+
SelectLabel.displayName = "SelectLabel";
|
|
485
|
+
|
|
486
|
+
export const SelectSeparator = React.forwardRef<HTMLDivElement, { className?: string }>(
|
|
487
|
+
({ className }, ref) => {
|
|
488
|
+
return (
|
|
489
|
+
<div
|
|
490
|
+
ref={ref}
|
|
491
|
+
className={cn("my-1 h-px bg-sec-200", className)}
|
|
492
|
+
data-testid="select-separator"
|
|
493
|
+
/>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
SelectSeparator.displayName = "SelectSeparator";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import React from 'react';
|
|
8
|
-
import { screen, waitFor } from '@testing-library/react';
|
|
8
|
+
import { screen, waitFor, act } from '@testing-library/react';
|
|
9
9
|
import userEvent from '@testing-library/user-event';
|
|
10
10
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
11
|
import {
|
|
@@ -66,11 +66,7 @@ describe('Select Component', () => {
|
|
|
66
66
|
);
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
vi.clearAllMocks();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('Basic Rendering', () => {
|
|
69
|
+
describe('Basic Rendering', () => {
|
|
74
70
|
it('renders with default props', () => {
|
|
75
71
|
renderSelect();
|
|
76
72
|
|
|
@@ -366,7 +362,7 @@ describe('Select Component', () => {
|
|
|
366
362
|
|
|
367
363
|
await waitFor(() => {
|
|
368
364
|
expect(screen.queryByTestId('select-content')).not.toBeInTheDocument();
|
|
369
|
-
});
|
|
365
|
+
}, { interval: 10 });
|
|
370
366
|
});
|
|
371
367
|
|
|
372
368
|
it('updates value on item selection', async () => {
|
|
@@ -397,7 +393,7 @@ describe('Select Component', () => {
|
|
|
397
393
|
|
|
398
394
|
await waitFor(() => {
|
|
399
395
|
expect(screen.queryByTestId('select-content')).not.toBeInTheDocument();
|
|
400
|
-
});
|
|
396
|
+
}, { interval: 10 });
|
|
401
397
|
});
|
|
402
398
|
});
|
|
403
399
|
|
|
@@ -459,7 +455,7 @@ describe('Select Component', () => {
|
|
|
459
455
|
|
|
460
456
|
await waitFor(() => {
|
|
461
457
|
expect(screen.queryByTestId('select-content')).not.toBeInTheDocument();
|
|
462
|
-
});
|
|
458
|
+
}, { interval: 10 });
|
|
463
459
|
});
|
|
464
460
|
|
|
465
461
|
it('selects item on click', async () => {
|
|
@@ -513,29 +509,40 @@ describe('Select Component', () => {
|
|
|
513
509
|
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument();
|
|
514
510
|
});
|
|
515
511
|
|
|
516
|
-
it
|
|
512
|
+
it('filters options based on search term', async () => {
|
|
517
513
|
const user = userEvent.setup();
|
|
514
|
+
const options = [
|
|
515
|
+
{ value: 'apple', label: 'Apple' },
|
|
516
|
+
{ value: 'banana', label: 'Banana' },
|
|
517
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
518
|
+
];
|
|
519
|
+
|
|
518
520
|
renderWithProviders(
|
|
519
|
-
<Select>
|
|
521
|
+
<Select defaultOpen>
|
|
520
522
|
<SelectTrigger>
|
|
521
|
-
<SelectValue />
|
|
523
|
+
<SelectValue placeholder="Select an option..." />
|
|
522
524
|
</SelectTrigger>
|
|
523
525
|
<SelectContent searchable>
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
526
|
+
{options.map(option => (
|
|
527
|
+
<SelectItem key={option.value} value={option.value}>
|
|
528
|
+
{option.label}
|
|
529
|
+
</SelectItem>
|
|
530
|
+
))}
|
|
527
531
|
</SelectContent>
|
|
528
532
|
</Select>
|
|
529
533
|
);
|
|
530
534
|
|
|
531
535
|
const trigger = screen.getByTestId('select-trigger');
|
|
532
|
-
await user.click(trigger);
|
|
533
536
|
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
537
|
+
// Get search input - should be visible once dropdown opens
|
|
538
|
+
expect(screen.getByTestId('select-search-input')).toBeInTheDocument();
|
|
539
|
+
|
|
540
|
+
// Verify options are visible
|
|
541
|
+
expect(screen.getByText('Apple')).toBeInTheDocument();
|
|
542
|
+
expect(screen.getByText('Banana')).toBeInTheDocument();
|
|
543
|
+
expect(screen.getByText('Cherry')).toBeInTheDocument();
|
|
538
544
|
|
|
545
|
+
// Type in search input to filter
|
|
539
546
|
const searchInput = screen.getByTestId('select-search-input');
|
|
540
547
|
await user.type(searchInput, 'app');
|
|
541
548
|
|
|
@@ -547,10 +554,10 @@ describe('Select Component', () => {
|
|
|
547
554
|
});
|
|
548
555
|
});
|
|
549
556
|
|
|
550
|
-
it
|
|
557
|
+
it('shows clear search button when search term exists', async () => {
|
|
551
558
|
const user = userEvent.setup();
|
|
552
559
|
renderWithProviders(
|
|
553
|
-
<Select>
|
|
560
|
+
<Select defaultOpen>
|
|
554
561
|
<SelectTrigger>
|
|
555
562
|
<SelectValue />
|
|
556
563
|
</SelectTrigger>
|
|
@@ -560,14 +567,6 @@ describe('Select Component', () => {
|
|
|
560
567
|
</Select>
|
|
561
568
|
);
|
|
562
569
|
|
|
563
|
-
const trigger = screen.getByTestId('select-trigger');
|
|
564
|
-
await user.click(trigger);
|
|
565
|
-
|
|
566
|
-
// Wait for dropdown to open and search input to be available
|
|
567
|
-
await waitFor(() => {
|
|
568
|
-
expect(screen.getByTestId('select-content')).toBeInTheDocument();
|
|
569
|
-
});
|
|
570
|
-
|
|
571
570
|
const searchInput = screen.getByTestId('select-search-input');
|
|
572
571
|
await user.type(searchInput, 'test');
|
|
573
572
|
|
|
@@ -577,10 +576,10 @@ describe('Select Component', () => {
|
|
|
577
576
|
});
|
|
578
577
|
});
|
|
579
578
|
|
|
580
|
-
it
|
|
579
|
+
it('clears search when clear button is clicked', async () => {
|
|
581
580
|
const user = userEvent.setup();
|
|
582
581
|
renderWithProviders(
|
|
583
|
-
<Select>
|
|
582
|
+
<Select defaultOpen>
|
|
584
583
|
<SelectTrigger>
|
|
585
584
|
<SelectValue />
|
|
586
585
|
</SelectTrigger>
|
|
@@ -591,14 +590,6 @@ describe('Select Component', () => {
|
|
|
591
590
|
</Select>
|
|
592
591
|
);
|
|
593
592
|
|
|
594
|
-
const trigger = screen.getByTestId('select-trigger');
|
|
595
|
-
await user.click(trigger);
|
|
596
|
-
|
|
597
|
-
// Wait for dropdown to open and search input to be available
|
|
598
|
-
await waitFor(() => {
|
|
599
|
-
expect(screen.getByTestId('select-content')).toBeInTheDocument();
|
|
600
|
-
});
|
|
601
|
-
|
|
602
593
|
const searchInput = screen.getByTestId('select-search-input');
|
|
603
594
|
await user.type(searchInput, 'app');
|
|
604
595
|
|
|
@@ -607,7 +598,11 @@ describe('Select Component', () => {
|
|
|
607
598
|
expect(screen.queryByText('Banana')).not.toBeInTheDocument();
|
|
608
599
|
});
|
|
609
600
|
|
|
610
|
-
|
|
601
|
+
// Wait for clear button to appear
|
|
602
|
+
const clearButton = await waitFor(() => {
|
|
603
|
+
return screen.getByTestId('select-clear-search');
|
|
604
|
+
});
|
|
605
|
+
|
|
611
606
|
await user.click(clearButton);
|
|
612
607
|
|
|
613
608
|
// Wait for search to be cleared and all options to be visible again
|
|
@@ -617,10 +612,10 @@ describe('Select Component', () => {
|
|
|
617
612
|
});
|
|
618
613
|
});
|
|
619
614
|
|
|
620
|
-
it
|
|
615
|
+
it('clears search on Escape key', async () => {
|
|
621
616
|
const user = userEvent.setup();
|
|
622
617
|
renderWithProviders(
|
|
623
|
-
<Select>
|
|
618
|
+
<Select defaultOpen>
|
|
624
619
|
<SelectTrigger>
|
|
625
620
|
<SelectValue />
|
|
626
621
|
</SelectTrigger>
|
|
@@ -631,16 +626,14 @@ describe('Select Component', () => {
|
|
|
631
626
|
</Select>
|
|
632
627
|
);
|
|
633
628
|
|
|
634
|
-
const
|
|
635
|
-
await user.
|
|
629
|
+
const searchInput = screen.getByTestId('select-search-input');
|
|
630
|
+
await user.type(searchInput, 'app');
|
|
636
631
|
|
|
637
|
-
// Wait for
|
|
632
|
+
// Wait for filtering to complete
|
|
638
633
|
await waitFor(() => {
|
|
639
|
-
expect(screen.
|
|
634
|
+
expect(screen.queryByText('Banana')).not.toBeInTheDocument();
|
|
640
635
|
});
|
|
641
636
|
|
|
642
|
-
const searchInput = screen.getByTestId('select-search-input');
|
|
643
|
-
await user.type(searchInput, 'app');
|
|
644
637
|
await user.keyboard('{Escape}');
|
|
645
638
|
|
|
646
639
|
// Wait for search to be cleared and all options to be visible again
|