@jmruthers/pace-core 0.6.6 → 0.6.8
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/{scripts/audit/audit-dependencies.cjs → audit-tool/00-dependencies.cjs} +227 -22
- package/audit-tool/audits/01-pace-core-compliance.cjs +556 -0
- package/audit-tool/audits/02-project-structure.cjs +240 -0
- package/audit-tool/audits/03-architecture.cjs +224 -0
- package/audit-tool/audits/04-code-quality.cjs +149 -0
- package/audit-tool/audits/05-styling.cjs +224 -0
- package/audit-tool/audits/06-security-rbac.cjs +554 -0
- package/audit-tool/audits/07-api-tech-stack.cjs +355 -0
- package/audit-tool/audits/08-testing-documentation.cjs +202 -0
- package/audit-tool/audits/09-operations.cjs +208 -0
- package/audit-tool/index.cjs +295 -0
- package/audit-tool/utils/code-utils.cjs +218 -0
- package/audit-tool/utils/file-utils.cjs +230 -0
- package/audit-tool/utils/report-utils.cjs +380 -0
- package/cursor-rules/00-standards-overview.mdc +156 -0
- package/cursor-rules/{00-pace-core-compliance.mdc → 01-pace-core-compliance.mdc} +187 -34
- package/cursor-rules/02-project-structure.mdc +37 -5
- package/cursor-rules/{03-solid-principles.mdc → 03-architecture.mdc} +125 -11
- package/cursor-rules/04-code-quality.mdc +419 -0
- package/cursor-rules/{08-markup-quality.mdc → 05-styling.mdc} +55 -10
- package/cursor-rules/{09-rbac-compliance.mdc → 06-security-rbac.mdc} +62 -6
- package/cursor-rules/07-api-tech-stack.mdc +377 -0
- package/cursor-rules/08-testing-documentation.mdc +324 -0
- package/cursor-rules/09-operations.mdc +365 -0
- package/dist/DataTable-6RMSCQJ6.js +15 -0
- package/dist/{DataTable-2N_tqbfq.d.ts → DataTable-DRUIgtUH.d.ts} +1 -1
- package/dist/{PublicPageProvider-BBH6Vqg7.d.ts → PublicPageProvider-CIGSujI2.d.ts} +40 -24
- package/dist/{UnifiedAuthProvider-ZT6TIGM7.js → UnifiedAuthProvider-7SNDOWYD.js} +2 -2
- package/dist/{api-Y4MQWOFW.js → api-7P7DI652.js} +1 -1
- package/dist/{chunk-MAGBIDNS.js → chunk-4DDCYDQ3.js} +8 -7
- package/dist/{chunk-BVP2BCJF.js → chunk-5W2A3DRC.js} +10 -9
- package/dist/{chunk-SD6WQY43.js → chunk-7ILTDCL2.js} +9 -1
- package/dist/{chunk-3QC3KRHK.js → chunk-A3W6LW53.js} +16 -1
- package/dist/{chunk-3O3WHILE.js → chunk-EF2UGZWY.js} +239 -63
- package/dist/{chunk-LAZMKTTF.js → chunk-EURB7QFZ.js} +341 -337
- package/dist/{chunk-2HGJFNAH.js → chunk-FEJLJNWA.js} +1 -15
- package/dist/{chunk-7TYHROIV.js → chunk-GS5672WG.js} +55 -13
- package/dist/{chunk-UIYSCEV7.js → chunk-IUBRCBSY.js} +1 -1
- package/dist/{chunk-ZFYPMX46.js → chunk-LX6U42O3.js} +1 -1
- package/dist/{chunk-FENMYN2U.js → chunk-MPBLMWVR.js} +3 -3
- package/dist/{chunk-ZS5VO5JB.js → chunk-NKHKXPI4.js} +408 -453
- package/dist/{chunk-A55DK444.js → chunk-OJ4SKRSV.js} +1 -7
- package/dist/{chunk-4T7OBVTU.js → chunk-S6ZQKDY6.js} +1 -1
- package/dist/{chunk-FTCRZOG2.js → chunk-T5CVK4R3.js} +5 -5
- package/dist/{chunk-OHIK3MIO.js → chunk-Z2FNRKF3.js} +13 -13
- package/dist/components.d.ts +5 -4
- package/dist/components.js +29 -34
- package/dist/eslint-rules/index.cjs +22 -9
- package/{src/eslint-rules/rules/compliance.cjs → dist/eslint-rules/rules/01-pace-core-compliance.cjs} +184 -23
- package/dist/eslint-rules/rules/04-code-quality.cjs +346 -0
- package/dist/eslint-rules/rules/05-styling.cjs +61 -0
- package/dist/eslint-rules/rules/{rbac.cjs → 06-security-rbac.cjs} +34 -13
- package/dist/eslint-rules/rules/07-api-tech-stack.cjs +385 -0
- package/dist/eslint-rules/rules/08-testing.cjs +94 -0
- package/dist/{functions-DHebl8-F.d.ts → functions-lBy5L2ry.d.ts} +1 -1
- package/dist/hooks.d.ts +5 -5
- package/dist/hooks.js +8 -8
- package/dist/index.d.ts +7 -7
- package/dist/index.js +21 -20
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +1 -1
- package/dist/rbac/index.js +8 -8
- package/dist/theming/runtime.d.ts +61 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/{types-B-K_5VnO.d.ts → types-DXstZpNI.d.ts} +0 -17
- package/dist/types.d.ts +2 -2
- package/dist/{usePublicRouteParams-COZ28Mvq.d.ts → usePublicRouteParams-MamNgwqe.d.ts} +19 -19
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +8 -8
- package/docs/README.md +1 -1
- package/docs/api/modules.md +106 -41
- package/docs/api-reference/components.md +18 -20
- package/docs/api-reference/hooks.md +80 -80
- package/docs/api-reference/types.md +1 -1
- package/docs/api-reference/utilities.md +1 -1
- package/docs/architecture/README.md +1 -1
- package/docs/core-concepts/events.md +3 -3
- package/docs/core-concepts/organisations.md +6 -6
- package/docs/core-concepts/permissions.md +6 -6
- package/docs/documentation-index.md +12 -18
- package/docs/getting-started/dependencies.md +23 -0
- package/docs/getting-started/documentation-index.md +1 -1
- package/docs/getting-started/examples/README.md +4 -4
- package/docs/getting-started/examples/full-featured-app.md +1 -1
- package/docs/getting-started/faq.md +2 -2
- package/docs/getting-started/quick-reference.md +4 -4
- package/docs/implementation-guides/app-layout.md +1 -1
- package/docs/implementation-guides/authentication.md +15 -15
- package/docs/implementation-guides/component-styling.md +1 -1
- package/docs/implementation-guides/data-tables.md +127 -34
- package/docs/implementation-guides/datatable-rbac-usage.md +1 -1
- package/docs/implementation-guides/dynamic-colors.md +3 -3
- package/docs/implementation-guides/file-upload-storage.md +2 -2
- package/docs/implementation-guides/hierarchical-datatable.md +40 -60
- package/docs/implementation-guides/inactivity-tracking.md +3 -3
- package/docs/implementation-guides/large-datasets.md +3 -2
- package/docs/implementation-guides/organisation-security.md +2 -2
- package/docs/implementation-guides/performance.md +2 -2
- package/docs/implementation-guides/permission-enforcement.md +1 -1
- package/docs/migration/V0.3.44_organisation-context-timing-fix.md +1 -1
- package/docs/migration/V0.4.0_rbac-migration.md +6 -6
- package/docs/rbac/README.md +5 -5
- package/docs/rbac/advanced-patterns.md +6 -6
- package/docs/rbac/api-reference.md +20 -20
- package/docs/rbac/event-based-apps.md +3 -3
- package/docs/rbac/examples.md +41 -41
- package/docs/rbac/getting-started.md +37 -37
- package/docs/rbac/performance.md +1 -1
- package/docs/rbac/quick-start.md +52 -52
- package/docs/rbac/secure-client-protection.md +1 -1
- package/docs/rbac/troubleshooting.md +1 -1
- package/docs/security/README.md +5 -5
- package/docs/standards/0-standards-overview.md +220 -0
- package/docs/standards/{00-pace-core-compliance.md → 1-pace-core-compliance-standards.md} +241 -185
- package/docs/standards/{02-project-structure.md → 2-project-structure-standards.md} +11 -47
- package/docs/standards/3-architecture-standards.md +606 -0
- package/docs/standards/4-code-quality-standards.md +728 -0
- package/docs/standards/{08-markup-quality.md → 5-styling-standards.md} +12 -9
- package/docs/standards/{09-rbac-compliance.md → 6-security-rbac-standards.md} +126 -18
- package/docs/standards/7-api-tech-stack-standards.md +662 -0
- package/docs/standards/8-testing-documentation-standards.md +401 -0
- package/docs/standards/9-operations-standards.md +1102 -0
- package/docs/standards/README.md +203 -104
- package/docs/troubleshooting/README.md +4 -4
- package/docs/troubleshooting/common-issues.md +2 -2
- package/docs/troubleshooting/debugging.md +9 -9
- package/docs/troubleshooting/migration.md +4 -4
- package/eslint-config-pace-core.cjs +50 -20
- package/package.json +50 -19
- package/scripts/eslint-audit.cjs +123 -0
- package/scripts/install-cursor-rules.cjs +11 -243
- package/scripts/install-eslint-config.cjs +349 -0
- package/scripts/validate-dependencies.cjs +248 -0
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +2 -2
- package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -2
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +30 -18
- package/src/__tests__/integration/UserProfile.test.tsx +14 -14
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -6
- package/src/__tests__/templates/accessibility.test.template.tsx +10 -9
- package/src/__tests__/templates/component.test.template.tsx +18 -15
- package/src/components/AddressField/AddressField.tsx +26 -1
- package/src/components/Alert/Alert.test.tsx +86 -22
- package/src/components/Alert/Alert.tsx +19 -11
- package/src/components/Badge/Badge.tsx +1 -1
- package/src/components/Calendar/Calendar.tsx +201 -47
- package/src/components/Checkbox/Checkbox.test.tsx +2 -1
- package/src/components/ContextSelector/ContextSelector.tsx +108 -126
- package/src/components/DataTable/AUDIT_REPORT.md +293 -0
- package/src/components/DataTable/DataTable.tsx +1 -19
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +6 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +21 -6
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +3 -2
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +9 -9
- package/src/components/DataTable/components/ColumnFilter.tsx +63 -74
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +43 -41
- package/src/components/DataTable/components/DataTableErrorBoundary.tsx +9 -11
- package/src/components/DataTable/components/DataTableLayout.tsx +5 -16
- package/src/components/DataTable/components/EditableRow.tsx +5 -7
- package/src/components/DataTable/components/EmptyState.tsx +11 -10
- package/src/components/DataTable/components/FilterRow.tsx +2 -4
- package/src/components/DataTable/components/ImportModal.tsx +124 -126
- package/src/components/DataTable/components/LoadingState.tsx +5 -6
- package/src/components/DataTable/components/SortIndicator.tsx +50 -0
- package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +4 -4
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +23 -82
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +37 -9
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +7 -4
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +12 -4
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +45 -27
- package/src/components/DataTable/components/index.ts +2 -1
- package/src/components/DataTable/types.ts +0 -18
- package/src/components/DataTable/utils/a11yUtils.ts +17 -0
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -1
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +11 -15
- package/src/components/DateTimeField/DateTimeField.tsx +7 -8
- package/src/components/Dialog/Dialog.test.tsx +1 -0
- package/src/components/Dialog/Dialog.tsx +25 -8
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +77 -79
- package/src/components/FileUpload/FileUpload.test.tsx +45 -16
- package/src/components/FileUpload/FileUpload.tsx +141 -130
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +48 -12
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +9 -9
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +30 -30
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +4 -4
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +7 -1
- package/src/components/Progress/Progress.tsx +2 -4
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +8 -8
- package/src/components/Select/Select.tsx +86 -77
- package/src/components/Select/types.ts +3 -0
- package/src/hooks/__tests__/ServiceHooks.test.tsx +16 -16
- package/src/hooks/__tests__/hooks.integration.test.tsx +49 -49
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +8 -5
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +4 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +99 -99
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +45 -8
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +22 -2
- package/src/hooks/public/usePublicEvent.ts +5 -5
- package/src/hooks/public/usePublicEventLogo.ts +5 -5
- package/src/hooks/public/usePublicFileDisplay.ts +2 -2
- package/src/hooks/public/usePublicRouteParams.ts +13 -9
- package/src/hooks/useAddressAutocomplete.test.ts +18 -18
- package/src/hooks/useAppConfig.ts +2 -2
- package/src/hooks/useEventTheme.test.ts +7 -7
- package/src/hooks/useEventTheme.ts +2 -1
- package/src/hooks/useFileDisplay.ts +2 -2
- package/src/hooks/useFileUrl.ts +52 -8
- package/src/hooks/useOrganisationSecurity.test.ts +2 -1
- package/src/providers/UnifiedAuthProvider.smoke.test.tsx +21 -21
- package/src/providers/__tests__/AuthProvider.test.tsx +21 -21
- package/src/providers/__tests__/EventProvider.test.tsx +61 -61
- package/src/providers/__tests__/InactivityProvider.test.tsx +56 -56
- package/src/providers/__tests__/OrganisationProvider.test.tsx +75 -75
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +38 -38
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +103 -103
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +7 -7
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +10 -10
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +15 -6
- package/src/rbac/__tests__/rbac-functions.test.ts +3 -3
- package/src/rbac/api.test.ts +104 -0
- package/src/rbac/engine.ts +1 -1
- package/src/rbac/hooks/useCan.test.ts +2 -2
- package/src/rbac/secureClient.ts +1 -1
- package/src/rbac/types/functions.ts +1 -1
- package/src/styles/core.css +7 -0
- package/src/theming/__tests__/parseEventColours.test.ts +118 -3
- package/src/theming/parseEventColours.ts +77 -11
- package/src/types/supabase.ts +2 -3
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +9 -9
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
- package/src/utils/file-reference/__tests__/file-reference.test.ts +4 -0
- package/src/utils/formatting/formatDate.test.ts +3 -2
- package/src/utils/formatting/formatDateTime.test.ts +2 -2
- package/src/utils/google-places/googlePlacesUtils.test.ts +36 -24
- package/src/utils/storage/README.md +1 -1
- package/src/utils/storage/__tests__/helpers.unit.test.ts +19 -12
- package/src/utils/storage/helpers.test.ts +69 -3
- package/cursor-rules/01-standards-compliance.mdc +0 -285
- package/cursor-rules/04-testing-standards.mdc +0 -270
- package/cursor-rules/05-bug-reports-and-features.mdc +0 -248
- package/cursor-rules/06-code-quality.mdc +0 -311
- package/cursor-rules/07-tech-stack-compliance.mdc +0 -216
- package/cursor-rules/10-error-handling-patterns.mdc +0 -179
- package/cursor-rules/11-performance-optimization.mdc +0 -169
- package/cursor-rules/12-ci-cd-integration.mdc +0 -150
- package/dist/DataTable-LRJL4IRV.js +0 -15
- package/dist/eslint-rules/rules/compliance.cjs +0 -348
- package/dist/eslint-rules/rules/components.cjs +0 -113
- package/dist/eslint-rules/rules/imports.cjs +0 -102
- package/docs/best-practices/README.md +0 -472
- package/docs/best-practices/accessibility.md +0 -604
- package/docs/best-practices/common-patterns.md +0 -516
- package/docs/best-practices/deployment.md +0 -1103
- package/docs/best-practices/performance.md +0 -1328
- package/docs/best-practices/security.md +0 -940
- package/docs/best-practices/testing.md +0 -1034
- package/docs/rbac/compliance/compliance-guide.md +0 -544
- package/docs/standards/01-standards-compliance.md +0 -188
- package/docs/standards/03-solid-principles.md +0 -39
- package/docs/standards/04-testing-standards.md +0 -36
- package/docs/standards/05-bug-reports-and-features.md +0 -27
- package/docs/standards/06-code-quality.md +0 -34
- package/docs/standards/07-tech-stack-compliance.md +0 -30
- package/docs/standards/10-error-handling-patterns.md +0 -401
- package/docs/standards/11-performance-optimization.md +0 -348
- package/docs/standards/12-ci-cd-integration.md +0 -370
- package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +0 -192
- package/scripts/audit/audit-compliance.cjs +0 -1295
- package/scripts/audit/audit-components.cjs +0 -260
- package/scripts/audit/audit-rbac.cjs +0 -954
- package/scripts/audit/audit-standards.cjs +0 -1268
- package/scripts/audit/index.cjs +0 -1927
- package/src/components/DataTable/components/DataTableBody.tsx +0 -478
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -156
- package/src/components/DataTable/components/ExpandButton.tsx +0 -113
- package/src/components/DataTable/components/GroupHeader.tsx +0 -54
- package/src/components/DataTable/components/ViewRowModal.tsx +0 -68
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -525
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +0 -462
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +0 -393
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +0 -476
- package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +0 -128
- package/src/components/DataTable/core/DataTableContext.tsx +0 -216
- package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +0 -136
- package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +0 -570
- package/src/components/DataTable/hooks/useColumnReordering.ts +0 -123
- package/src/components/DataTable/utils/debugTools.ts +0 -514
- package/src/eslint-rules/index.cjs +0 -22
- package/src/eslint-rules/rules/components.cjs +0 -113
- package/src/eslint-rules/rules/imports.cjs +0 -102
- package/src/eslint-rules/rules/rbac.cjs +0 -790
- package/src/eslint-rules/utils/helpers.cjs +0 -42
- package/src/eslint-rules/utils/manifest-loader.cjs +0 -75
|
@@ -11,7 +11,7 @@ import { renderWithProviders } from '../../__tests__/helpers/test-utils';
|
|
|
11
11
|
|
|
12
12
|
describe('Alert Component', () => {
|
|
13
13
|
describe('Rendering', () => {
|
|
14
|
-
it('renders as semantic
|
|
14
|
+
it('renders as semantic p element with role="alert"', () => {
|
|
15
15
|
renderWithProviders(
|
|
16
16
|
<Alert>
|
|
17
17
|
<AlertTitle>Test Title</AlertTitle>
|
|
@@ -20,8 +20,9 @@ describe('Alert Component', () => {
|
|
|
20
20
|
);
|
|
21
21
|
|
|
22
22
|
const alert = screen.getByRole('alert');
|
|
23
|
-
expect(alert.tagName).toBe('
|
|
23
|
+
expect(alert.tagName).toBe('P');
|
|
24
24
|
expect(alert).toBeInTheDocument();
|
|
25
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
it('renders with default variant', () => {
|
|
@@ -34,7 +35,8 @@ describe('Alert Component', () => {
|
|
|
34
35
|
|
|
35
36
|
const alert = screen.getByRole('alert');
|
|
36
37
|
expect(alert).toBeInTheDocument();
|
|
37
|
-
expect(alert.tagName).toBe('
|
|
38
|
+
expect(alert.tagName).toBe('P');
|
|
39
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
38
40
|
expect(alert).toHaveClass('relative', 'w-full', 'rounded-lg', 'border', 'p-4');
|
|
39
41
|
});
|
|
40
42
|
|
|
@@ -48,7 +50,8 @@ describe('Alert Component', () => {
|
|
|
48
50
|
|
|
49
51
|
const alert = screen.getByRole('alert');
|
|
50
52
|
expect(alert).toBeInTheDocument();
|
|
51
|
-
expect(alert.tagName).toBe('
|
|
53
|
+
expect(alert.tagName).toBe('P');
|
|
54
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
52
55
|
expect(alert).toHaveClass('border-destructive', 'text-destructive');
|
|
53
56
|
});
|
|
54
57
|
|
|
@@ -73,7 +76,8 @@ describe('Alert Component', () => {
|
|
|
73
76
|
);
|
|
74
77
|
|
|
75
78
|
const alert = screen.getByRole('alert');
|
|
76
|
-
expect(alert.tagName).toBe('
|
|
79
|
+
expect(alert.tagName).toBe('P');
|
|
80
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
77
81
|
expect(alert).toHaveClass('custom-alert-class');
|
|
78
82
|
});
|
|
79
83
|
|
|
@@ -88,7 +92,7 @@ describe('Alert Component', () => {
|
|
|
88
92
|
});
|
|
89
93
|
|
|
90
94
|
it('forwards ref correctly', () => {
|
|
91
|
-
const ref = React.createRef<
|
|
95
|
+
const ref = React.createRef<HTMLParagraphElement>();
|
|
92
96
|
|
|
93
97
|
renderWithProviders(
|
|
94
98
|
<Alert ref={ref}>
|
|
@@ -96,8 +100,8 @@ describe('Alert Component', () => {
|
|
|
96
100
|
</Alert>
|
|
97
101
|
);
|
|
98
102
|
|
|
99
|
-
expect(ref.current).toBeInstanceOf(
|
|
100
|
-
expect(ref.current?.tagName).toBe('
|
|
103
|
+
expect(ref.current).toBeInstanceOf(HTMLParagraphElement);
|
|
104
|
+
expect(ref.current?.tagName).toBe('P');
|
|
101
105
|
expect(ref.current).toHaveAttribute('role', 'alert');
|
|
102
106
|
});
|
|
103
107
|
});
|
|
@@ -275,7 +279,54 @@ describe('Alert Component', () => {
|
|
|
275
279
|
|
|
276
280
|
const alert = screen.getByRole('alert');
|
|
277
281
|
expect(alert).toBeInTheDocument();
|
|
278
|
-
expect(alert.tagName).toBe('
|
|
282
|
+
expect(alert.tagName).toBe('P');
|
|
283
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('supports role="status" prop for informational messages', () => {
|
|
287
|
+
renderWithProviders(
|
|
288
|
+
<Alert role="status" aria-live="polite">
|
|
289
|
+
<AlertTitle>Status Message</AlertTitle>
|
|
290
|
+
<AlertDescription>This is a status message</AlertDescription>
|
|
291
|
+
</Alert>
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const status = screen.getByRole('status');
|
|
295
|
+
expect(status).toBeInTheDocument();
|
|
296
|
+
expect(status.tagName).toBe('P');
|
|
297
|
+
expect(status).toHaveAttribute('role', 'status');
|
|
298
|
+
expect(status).toHaveAttribute('aria-live', 'polite');
|
|
299
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('defaults to role="alert" when role prop not provided', () => {
|
|
303
|
+
renderWithProviders(
|
|
304
|
+
<Alert>
|
|
305
|
+
<AlertTitle>Default Alert</AlertTitle>
|
|
306
|
+
<AlertDescription>This should default to alert role</AlertDescription>
|
|
307
|
+
</Alert>
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const alert = screen.getByRole('alert');
|
|
311
|
+
expect(alert).toBeInTheDocument();
|
|
312
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
313
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('allows custom role values', () => {
|
|
317
|
+
renderWithProviders(
|
|
318
|
+
<Alert role="region" aria-label="Custom region">
|
|
319
|
+
<AlertTitle>Custom Role</AlertTitle>
|
|
320
|
+
<AlertDescription>This uses a custom role</AlertDescription>
|
|
321
|
+
</Alert>
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const region = screen.getByRole('region', { name: 'Custom region' });
|
|
325
|
+
expect(region).toBeInTheDocument();
|
|
326
|
+
expect(region.tagName).toBe('P');
|
|
327
|
+
expect(region).toHaveAttribute('role', 'region');
|
|
328
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
329
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
279
330
|
});
|
|
280
331
|
|
|
281
332
|
it('does not have role="alert" for inline variant', () => {
|
|
@@ -301,7 +352,8 @@ describe('Alert Component', () => {
|
|
|
301
352
|
|
|
302
353
|
const alert = screen.getByRole('alert');
|
|
303
354
|
expect(alert).toBeInTheDocument();
|
|
304
|
-
expect(alert.tagName).toBe('
|
|
355
|
+
expect(alert.tagName).toBe('P');
|
|
356
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
305
357
|
|
|
306
358
|
// Screen readers will announce the content within the alert
|
|
307
359
|
expect(screen.getByText('Important Notice')).toBeInTheDocument();
|
|
@@ -320,7 +372,8 @@ describe('Alert Component', () => {
|
|
|
320
372
|
const title = screen.getByRole('heading', { level: 5 });
|
|
321
373
|
const description = screen.getByText('Semantic description with proper heading structure');
|
|
322
374
|
|
|
323
|
-
expect(alert.tagName).toBe('
|
|
375
|
+
expect(alert.tagName).toBe('P');
|
|
376
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
324
377
|
expect(title).toBeInTheDocument();
|
|
325
378
|
expect(description.tagName).toBe('P');
|
|
326
379
|
});
|
|
@@ -339,7 +392,8 @@ describe('Alert Component', () => {
|
|
|
339
392
|
|
|
340
393
|
const alert = screen.getByRole('alert');
|
|
341
394
|
expect(alert).toBeInTheDocument();
|
|
342
|
-
expect(alert.tagName).toBe('
|
|
395
|
+
expect(alert.tagName).toBe('P');
|
|
396
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
343
397
|
expect(screen.getByText('⚠️')).toBeInTheDocument();
|
|
344
398
|
expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument();
|
|
345
399
|
});
|
|
@@ -355,7 +409,8 @@ describe('Alert Component', () => {
|
|
|
355
409
|
|
|
356
410
|
const alert = screen.getByRole('alert');
|
|
357
411
|
expect(alert).toBeInTheDocument();
|
|
358
|
-
expect(alert.tagName).toBe('
|
|
412
|
+
expect(alert.tagName).toBe('P');
|
|
413
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
359
414
|
expect(screen.getByText('First description')).toBeInTheDocument();
|
|
360
415
|
expect(screen.getByText('Second description')).toBeInTheDocument();
|
|
361
416
|
});
|
|
@@ -369,7 +424,8 @@ describe('Alert Component', () => {
|
|
|
369
424
|
|
|
370
425
|
const alert = screen.getByRole('alert');
|
|
371
426
|
expect(alert).toBeInTheDocument();
|
|
372
|
-
expect(alert.tagName).toBe('
|
|
427
|
+
expect(alert.tagName).toBe('P');
|
|
428
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
373
429
|
expect(screen.getByText('Description without title')).toBeInTheDocument();
|
|
374
430
|
});
|
|
375
431
|
|
|
@@ -382,7 +438,8 @@ describe('Alert Component', () => {
|
|
|
382
438
|
|
|
383
439
|
const alert = screen.getByRole('alert');
|
|
384
440
|
expect(alert).toBeInTheDocument();
|
|
385
|
-
expect(alert.tagName).toBe('
|
|
441
|
+
expect(alert.tagName).toBe('P');
|
|
442
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
386
443
|
expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent('Title without description');
|
|
387
444
|
});
|
|
388
445
|
});
|
|
@@ -393,7 +450,8 @@ describe('Alert Component', () => {
|
|
|
393
450
|
|
|
394
451
|
const alert = screen.getByRole('alert');
|
|
395
452
|
expect(alert).toBeInTheDocument();
|
|
396
|
-
expect(alert.tagName).toBe('
|
|
453
|
+
expect(alert.tagName).toBe('P');
|
|
454
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
397
455
|
expect(alert).toBeEmptyDOMElement();
|
|
398
456
|
});
|
|
399
457
|
|
|
@@ -423,7 +481,8 @@ describe('Alert Component', () => {
|
|
|
423
481
|
// Should fallback to default behavior
|
|
424
482
|
const alert = screen.getByRole('alert');
|
|
425
483
|
expect(alert).toBeInTheDocument();
|
|
426
|
-
expect(alert.tagName).toBe('
|
|
484
|
+
expect(alert.tagName).toBe('P');
|
|
485
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
427
486
|
});
|
|
428
487
|
|
|
429
488
|
it('handles rapid variant changes', () => {
|
|
@@ -454,7 +513,8 @@ describe('Alert Component', () => {
|
|
|
454
513
|
|
|
455
514
|
const alert = screen.getByRole('alert');
|
|
456
515
|
expect(alert).toBeInTheDocument();
|
|
457
|
-
expect(alert.tagName).toBe('
|
|
516
|
+
expect(alert.tagName).toBe('P');
|
|
517
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
458
518
|
});
|
|
459
519
|
});
|
|
460
520
|
|
|
@@ -473,7 +533,8 @@ describe('Alert Component', () => {
|
|
|
473
533
|
|
|
474
534
|
const alert = screen.getByRole('alert');
|
|
475
535
|
expect(alert).toBeInTheDocument();
|
|
476
|
-
expect(alert.tagName).toBe('
|
|
536
|
+
expect(alert.tagName).toBe('P');
|
|
537
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
477
538
|
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
478
539
|
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
|
|
479
540
|
});
|
|
@@ -494,8 +555,10 @@ describe('Alert Component', () => {
|
|
|
494
555
|
|
|
495
556
|
const alerts = screen.getAllByRole('alert');
|
|
496
557
|
expect(alerts).toHaveLength(2);
|
|
497
|
-
expect(alerts[0].tagName).toBe('
|
|
498
|
-
expect(alerts[1].tagName).toBe('
|
|
558
|
+
expect(alerts[0].tagName).toBe('P');
|
|
559
|
+
expect(alerts[1].tagName).toBe('P');
|
|
560
|
+
expect(alerts[0]).toHaveAttribute('role', 'alert');
|
|
561
|
+
expect(alerts[1]).toHaveAttribute('role', 'alert');
|
|
499
562
|
expect(alerts[0]).toHaveClass('bg-background', 'text-foreground');
|
|
500
563
|
expect(alerts[1]).toHaveClass('border-destructive', 'text-destructive');
|
|
501
564
|
});
|
|
@@ -516,7 +579,8 @@ describe('Alert Component', () => {
|
|
|
516
579
|
|
|
517
580
|
const alert = screen.getByRole('alert');
|
|
518
581
|
expect(alert).toBeInTheDocument();
|
|
519
|
-
expect(alert.tagName).toBe('
|
|
582
|
+
expect(alert.tagName).toBe('P');
|
|
583
|
+
expect(alert).toHaveAttribute('role', 'alert');
|
|
520
584
|
expect(screen.getByText('Complex Alert')).toBeInTheDocument();
|
|
521
585
|
expect(screen.getByText('This is a complex description with')).toBeInTheDocument();
|
|
522
586
|
expect(screen.getByText('Multiple elements')).toBeInTheDocument();
|
|
@@ -10,27 +10,32 @@
|
|
|
10
10
|
* Features:
|
|
11
11
|
* - Multiple visual variants (default, destructive, inline)
|
|
12
12
|
* - Title and description support
|
|
13
|
-
* - Semantic HTML: renders as `<
|
|
14
|
-
* - ARIA role="alert" for accessibility
|
|
13
|
+
* - Semantic HTML: renders as `<p>` element with `role="alert"` (default) or custom role
|
|
15
14
|
* - Keyboard and screen reader accessible
|
|
16
15
|
* - Composable with icons and actions
|
|
17
16
|
* - Inline variant for lightweight text formatting
|
|
18
17
|
*
|
|
19
18
|
* @example
|
|
20
19
|
* ```tsx
|
|
21
|
-
* // Basic alert (renders as <
|
|
20
|
+
* // Basic alert (renders as <p role="alert"> with <h5> title and <p> description)
|
|
22
21
|
* <Alert>
|
|
23
22
|
* <AlertTitle>Success</AlertTitle>
|
|
24
23
|
* <AlertDescription>Your changes have been saved.</AlertDescription>
|
|
25
24
|
* </Alert>
|
|
26
25
|
*
|
|
27
|
-
* // Destructive alert with icon (renders as <
|
|
26
|
+
* // Destructive alert with icon (renders as <p role="alert"> with <h5> title and <p> description)
|
|
28
27
|
* <Alert variant="destructive">
|
|
29
28
|
* <ErrorIcon />
|
|
30
29
|
* <AlertTitle>Error</AlertTitle>
|
|
31
30
|
* <AlertDescription>Something went wrong.</AlertDescription>
|
|
32
31
|
* </Alert>
|
|
33
32
|
*
|
|
33
|
+
* // Status message (renders as <p role="status"> for informational messages)
|
|
34
|
+
* <Alert role="status" aria-live="polite">
|
|
35
|
+
* <AlertTitle>No data available</AlertTitle>
|
|
36
|
+
* <AlertDescription>Get started by adding your first entry.</AlertDescription>
|
|
37
|
+
* </Alert>
|
|
38
|
+
*
|
|
34
39
|
* // Inline alert (renders as React.Fragment with <strong> title and <span> description)
|
|
35
40
|
* <Alert variant="inline">
|
|
36
41
|
* <AlertTitle>Note:</AlertTitle>
|
|
@@ -39,8 +44,8 @@
|
|
|
39
44
|
* ```
|
|
40
45
|
*
|
|
41
46
|
* @accessibility
|
|
42
|
-
* - Uses semantic HTML: `<
|
|
43
|
-
* -
|
|
47
|
+
* - Uses semantic HTML: `<p>` element with `role="alert"` (default) for screen reader announcements
|
|
48
|
+
* - Can be customized with `role="status"` for informational messages
|
|
44
49
|
* - Title and description are semantically structured
|
|
45
50
|
* - Supports keyboard navigation and focus
|
|
46
51
|
*/
|
|
@@ -65,9 +70,12 @@ const getAlertClasses = (variant: "default" | "destructive" | "inline" = "defaul
|
|
|
65
70
|
};
|
|
66
71
|
|
|
67
72
|
const Alert = React.forwardRef<
|
|
68
|
-
|
|
69
|
-
React.HTMLAttributes<
|
|
70
|
-
|
|
73
|
+
HTMLParagraphElement,
|
|
74
|
+
React.HTMLAttributes<HTMLParagraphElement> & {
|
|
75
|
+
variant?: "default" | "destructive" | "inline";
|
|
76
|
+
role?: string;
|
|
77
|
+
}
|
|
78
|
+
>(({ className, variant = "default", role = "alert", ...props }, ref) => {
|
|
71
79
|
const contextValue = React.useMemo(() => ({ variant }), [variant])
|
|
72
80
|
|
|
73
81
|
if (variant === "inline") {
|
|
@@ -80,10 +88,10 @@ const Alert = React.forwardRef<
|
|
|
80
88
|
|
|
81
89
|
return (
|
|
82
90
|
<AlertContext.Provider value={contextValue}>
|
|
83
|
-
<
|
|
91
|
+
<p
|
|
84
92
|
ref={ref}
|
|
85
93
|
className={cn(getAlertClasses(variant), className)}
|
|
86
|
-
role=
|
|
94
|
+
role={role}
|
|
87
95
|
{...props}
|
|
88
96
|
/>
|
|
89
97
|
</AlertContext.Provider>
|
|
@@ -163,7 +163,7 @@ function buildVariantClasses(style: Style, color: Color, shade: Shade): string {
|
|
|
163
163
|
* Classes used: shadow-badge-soft shadow-main-200 shadow-main-500 shadow-main-700
|
|
164
164
|
* shadow-sec-200 shadow-sec-500 shadow-sec-700 shadow-acc-200 shadow-acc-500 shadow-acc-700
|
|
165
165
|
*/
|
|
166
|
-
const
|
|
166
|
+
const _tailwindClassScan = [
|
|
167
167
|
// Solid background classes
|
|
168
168
|
'bg-main-100', 'bg-main-600', 'bg-main-900',
|
|
169
169
|
'bg-sec-100', 'bg-sec-600', 'bg-sec-900',
|
|
@@ -65,7 +65,9 @@ import {
|
|
|
65
65
|
type DateRange,
|
|
66
66
|
} from 'react-day-picker';
|
|
67
67
|
import { enAU } from 'date-fns/locale';
|
|
68
|
+
import { format } from 'date-fns';
|
|
68
69
|
import { cn } from '../../utils/core/cn';
|
|
70
|
+
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../Select';
|
|
69
71
|
|
|
70
72
|
// Define custom types for components that don't have exported types
|
|
71
73
|
type MonthGridProps = React.TableHTMLAttributes<HTMLTableElement>;
|
|
@@ -77,6 +79,9 @@ type MonthProps = {
|
|
|
77
79
|
displayIndex: number;
|
|
78
80
|
className?: string;
|
|
79
81
|
children?: React.ReactNode;
|
|
82
|
+
captionLayout?: DayPickerProps['captionLayout'];
|
|
83
|
+
startMonth?: Date;
|
|
84
|
+
endMonth?: Date;
|
|
80
85
|
};
|
|
81
86
|
type RootProps = {
|
|
82
87
|
children?: React.ReactNode;
|
|
@@ -190,7 +195,7 @@ const assignToRef = <T,>(ref: React.Ref<T | null> | undefined, value: T | null)
|
|
|
190
195
|
* ```
|
|
191
196
|
*/
|
|
192
197
|
const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
|
|
193
|
-
({ className, classNames, mode, components, locale, month: controlledMonth, onMonthChange: controlledOnMonthChange, onSelect, ...props }, ref) => {
|
|
198
|
+
({ className, classNames, mode, components, locale, month: controlledMonth, onMonthChange: controlledOnMonthChange, onSelect, captionLayout, startMonth, endMonth, ...props }, ref) => {
|
|
194
199
|
const tableRef = React.useRef<HTMLTableElement | null>(null);
|
|
195
200
|
const setForwardedRef = React.useCallback(
|
|
196
201
|
(node: HTMLTableElement | null) => {
|
|
@@ -267,9 +272,42 @@ const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
|
|
|
267
272
|
});
|
|
268
273
|
CustomRoot.displayName = 'CustomRoot';
|
|
269
274
|
|
|
270
|
-
// Custom Months: Remove wrapper div,
|
|
275
|
+
// Custom Months: Remove wrapper div, filter out MonthCaption and Dropdown when dropdown layout is used
|
|
271
276
|
const CustomMonths = React.memo(({ children }: MonthsProps) => {
|
|
272
|
-
|
|
277
|
+
// When captionLayout="dropdown", react-day-picker may render MonthCaption or Dropdown components
|
|
278
|
+
// Filter them out since we render our own dropdowns inside the table's <caption> element
|
|
279
|
+
const childrenArray = React.Children.toArray(children);
|
|
280
|
+
const filteredChildren = childrenArray.filter((child: any) => {
|
|
281
|
+
if (!React.isValidElement(child)) return true;
|
|
282
|
+
const childType = child.type as any;
|
|
283
|
+
const displayName = childType?.displayName || childType?.name;
|
|
284
|
+
// Filter out MonthCaption and any Dropdown-related components
|
|
285
|
+
if (displayName === 'MonthCaption' ||
|
|
286
|
+
displayName === 'Dropdown' ||
|
|
287
|
+
displayName === 'DropdownMonth' ||
|
|
288
|
+
displayName === 'DropdownYear' ||
|
|
289
|
+
(typeof childType === 'string' && childType.includes('dropdown'))) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
// Also check for the div wrapper that react-day-picker might render
|
|
293
|
+
if (childType === 'div') {
|
|
294
|
+
const childProps = child.props as { children?: React.ReactNode; [key: string]: unknown };
|
|
295
|
+
if (childProps?.children) {
|
|
296
|
+
const childChildren = React.Children.toArray(childProps.children);
|
|
297
|
+
// If it contains a span with role="status" and aria-live="polite", it's likely the default caption
|
|
298
|
+
const hasCaptionSpan = childChildren.some((cc: any) => {
|
|
299
|
+
if (!React.isValidElement(cc) || cc.type !== 'span') return false;
|
|
300
|
+
const spanProps = cc.props as { role?: string; 'aria-live'?: string; [key: string]: unknown };
|
|
301
|
+
return spanProps?.role === 'status' && spanProps?.['aria-live'] === 'polite';
|
|
302
|
+
});
|
|
303
|
+
if (hasCaptionSpan) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
});
|
|
310
|
+
return <>{filteredChildren}</>;
|
|
273
311
|
});
|
|
274
312
|
CustomMonths.displayName = 'CustomMonths';
|
|
275
313
|
|
|
@@ -277,9 +315,93 @@ const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
|
|
|
277
315
|
return <table ref={forwardedRef} {...props} />;
|
|
278
316
|
});
|
|
279
317
|
CustomMonthGrid.displayName = 'CustomMonthGrid';
|
|
280
|
-
|
|
318
|
+
|
|
319
|
+
// Custom MonthCaption: renders dropdowns for month/year selection
|
|
320
|
+
type MonthCaptionProps = {
|
|
321
|
+
displayMonth: Date;
|
|
322
|
+
startMonth?: Date;
|
|
323
|
+
endMonth?: Date;
|
|
324
|
+
locale?: DayPickerProps['locale'];
|
|
325
|
+
};
|
|
326
|
+
const CustomMonthCaption = React.memo(({ displayMonth, startMonth: captionStartMonth, endMonth: captionEndMonth, locale: captionLocale }: MonthCaptionProps) => {
|
|
327
|
+
const { goToMonth } = useDayPicker();
|
|
328
|
+
// Get locale from props (defaults to enAU)
|
|
329
|
+
const calendarLocale = (captionLocale || enAU) as typeof enAU;
|
|
330
|
+
|
|
331
|
+
// Get start and end months from props (passed via Calendar)
|
|
332
|
+
const fromDate = captionStartMonth || new Date(1900, 0);
|
|
333
|
+
const toDate = captionEndMonth || new Date(2100, 11);
|
|
334
|
+
|
|
335
|
+
// Generate month options using date-fns format
|
|
336
|
+
const monthOptions = React.useMemo(() => {
|
|
337
|
+
const months: { value: string; label: string }[] = [];
|
|
338
|
+
for (let i = 0; i < 12; i++) {
|
|
339
|
+
const monthDate = new Date(displayMonth.getFullYear(), i, 1);
|
|
340
|
+
const label = format(monthDate, 'MMMM', { locale: calendarLocale });
|
|
341
|
+
months.push({ value: i.toString(), label });
|
|
342
|
+
}
|
|
343
|
+
return months;
|
|
344
|
+
}, [calendarLocale, displayMonth]);
|
|
345
|
+
|
|
346
|
+
// Generate year options based on startMonth and endMonth
|
|
347
|
+
const yearOptions = React.useMemo(() => {
|
|
348
|
+
const years: { value: string; label: string }[] = [];
|
|
349
|
+
const startYear = fromDate.getFullYear();
|
|
350
|
+
const endYear = toDate.getFullYear();
|
|
351
|
+
for (let year = startYear; year <= endYear; year++) {
|
|
352
|
+
years.push({ value: year.toString(), label: year.toString() });
|
|
353
|
+
}
|
|
354
|
+
return years;
|
|
355
|
+
}, [fromDate, toDate]);
|
|
356
|
+
|
|
357
|
+
const currentMonth = displayMonth.getMonth();
|
|
358
|
+
const currentYear = displayMonth.getFullYear();
|
|
359
|
+
|
|
360
|
+
const handleMonthChange = React.useCallback((value: string) => {
|
|
361
|
+
const newMonth = parseInt(value, 10);
|
|
362
|
+
const newDate = new Date(currentYear, newMonth, 1);
|
|
363
|
+
goToMonth(newDate);
|
|
364
|
+
}, [currentYear, goToMonth]);
|
|
365
|
+
|
|
366
|
+
const handleYearChange = React.useCallback((value: string) => {
|
|
367
|
+
const newYear = parseInt(value, 10);
|
|
368
|
+
const newDate = new Date(newYear, currentMonth, 1);
|
|
369
|
+
goToMonth(newDate);
|
|
370
|
+
}, [currentMonth, goToMonth]);
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<nav className="relative flex items-center justify-center gap-2">
|
|
374
|
+
<Select value={currentMonth.toString()} onValueChange={handleMonthChange}>
|
|
375
|
+
<SelectTrigger className="w-auto min-w-[120px]">
|
|
376
|
+
<SelectValue />
|
|
377
|
+
</SelectTrigger>
|
|
378
|
+
<SelectContent>
|
|
379
|
+
{monthOptions.map((option) => (
|
|
380
|
+
<SelectItem key={option.value} value={option.value}>
|
|
381
|
+
{option.label}
|
|
382
|
+
</SelectItem>
|
|
383
|
+
))}
|
|
384
|
+
</SelectContent>
|
|
385
|
+
</Select>
|
|
386
|
+
<Select value={currentYear.toString()} onValueChange={handleYearChange}>
|
|
387
|
+
<SelectTrigger className="w-auto min-w-[100px]">
|
|
388
|
+
<SelectValue />
|
|
389
|
+
</SelectTrigger>
|
|
390
|
+
<SelectContent>
|
|
391
|
+
{yearOptions.map((option) => (
|
|
392
|
+
<SelectItem key={option.value} value={option.value}>
|
|
393
|
+
{option.label}
|
|
394
|
+
</SelectItem>
|
|
395
|
+
))}
|
|
396
|
+
</SelectContent>
|
|
397
|
+
</Select>
|
|
398
|
+
</nav>
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
CustomMonthCaption.displayName = 'CustomMonthCaption';
|
|
402
|
+
|
|
281
403
|
// Custom Month: inject caption + navigation directly inside the <table>
|
|
282
|
-
const CustomMonth = React.memo(({ calendarMonth, displayIndex, className, children }: MonthProps) => {
|
|
404
|
+
const CustomMonth = React.memo(({ calendarMonth, displayIndex, className, children, captionLayout: monthCaptionLayout, startMonth: monthStartMonth, endMonth: monthEndMonth }: MonthProps) => {
|
|
283
405
|
const { formatters, components, labels, classNames, previousMonth, nextMonth, goToMonth } = useDayPicker();
|
|
284
406
|
const caption = formatters.formatCaption(calendarMonth.date, {});
|
|
285
407
|
const Chevron = components?.Chevron;
|
|
@@ -367,6 +489,9 @@ const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
|
|
|
367
489
|
}
|
|
368
490
|
: undefined;
|
|
369
491
|
|
|
492
|
+
// Determine if we should render dropdowns or buttons
|
|
493
|
+
const isDropdownLayout = monthCaptionLayout === 'dropdown';
|
|
494
|
+
|
|
370
495
|
return React.cloneElement(
|
|
371
496
|
monthGridElement,
|
|
372
497
|
{
|
|
@@ -376,45 +501,54 @@ const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
|
|
|
376
501
|
},
|
|
377
502
|
<>
|
|
378
503
|
<caption className="relative">
|
|
379
|
-
|
|
380
|
-
<
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
504
|
+
{isDropdownLayout ? (
|
|
505
|
+
<CustomMonthCaption
|
|
506
|
+
displayMonth={calendarMonth.date}
|
|
507
|
+
startMonth={monthStartMonth}
|
|
508
|
+
endMonth={monthEndMonth}
|
|
509
|
+
locale={locale}
|
|
510
|
+
/>
|
|
511
|
+
) : (
|
|
512
|
+
<nav className="relative flex items-center justify-center gap-1">
|
|
513
|
+
<button
|
|
514
|
+
type="button"
|
|
515
|
+
className={cn(
|
|
516
|
+
'h-7 w-7 bg-transparent p-0',
|
|
517
|
+
'inline-flex items-center justify-center rounded-md',
|
|
518
|
+
'hover:bg-acc-100',
|
|
519
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2',
|
|
520
|
+
'disabled:opacity-50 disabled:pointer-events-none',
|
|
521
|
+
classNames?.button_previous
|
|
522
|
+
)}
|
|
523
|
+
tabIndex={previousMonth ? undefined : -1}
|
|
524
|
+
aria-disabled={previousMonth ? undefined : true}
|
|
525
|
+
aria-label={previousMonth ? labels.labelPrevious(previousMonth) : undefined}
|
|
526
|
+
onClick={handlePreviousClick}
|
|
527
|
+
disabled={!previousMonth}
|
|
528
|
+
>
|
|
529
|
+
{Chevron ? <Chevron orientation="left" className="size-4" disabled={!previousMonth} /> : <span>‹</span>}
|
|
530
|
+
</button>
|
|
531
|
+
<span className="text-sm font-medium">{caption}</span>
|
|
532
|
+
<button
|
|
533
|
+
type="button"
|
|
534
|
+
className={cn(
|
|
535
|
+
'h-7 w-7 bg-transparent p-0',
|
|
536
|
+
'inline-flex items-center justify-center rounded-md',
|
|
537
|
+
'hover:bg-acc-100',
|
|
538
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2',
|
|
539
|
+
'disabled:opacity-50 disabled:pointer-events-none',
|
|
540
|
+
classNames?.button_next
|
|
541
|
+
)}
|
|
542
|
+
tabIndex={nextMonth ? undefined : -1}
|
|
543
|
+
aria-disabled={nextMonth ? undefined : true}
|
|
544
|
+
aria-label={nextMonth ? labels.labelNext(nextMonth) : undefined}
|
|
545
|
+
onClick={handleNextClick}
|
|
546
|
+
disabled={!nextMonth}
|
|
547
|
+
>
|
|
548
|
+
{Chevron ? <Chevron orientation="right" className="size-4" disabled={!nextMonth} /> : <span>›</span>}
|
|
549
|
+
</button>
|
|
550
|
+
</nav>
|
|
551
|
+
)}
|
|
418
552
|
</caption>
|
|
419
553
|
{monthGridChildren}
|
|
420
554
|
</>
|
|
@@ -442,17 +576,37 @@ const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
|
|
|
442
576
|
});
|
|
443
577
|
CustomWeekdays.displayName = 'CustomWeekdays';
|
|
444
578
|
|
|
579
|
+
// Create a wrapper for CustomMonth that passes the required props
|
|
580
|
+
const CustomMonthWithProps = React.useCallback((props: MonthProps) => {
|
|
581
|
+
return (
|
|
582
|
+
<CustomMonth
|
|
583
|
+
{...props}
|
|
584
|
+
captionLayout={captionLayout}
|
|
585
|
+
startMonth={startMonth}
|
|
586
|
+
endMonth={endMonth}
|
|
587
|
+
/>
|
|
588
|
+
);
|
|
589
|
+
}, [captionLayout, startMonth, endMonth]);
|
|
590
|
+
|
|
591
|
+
// Custom MonthCaption wrapper: returns null to prevent default rendering
|
|
592
|
+
// The actual caption is rendered inside CustomMonth within the table's <caption> element
|
|
593
|
+
const CustomMonthCaptionWrapper = React.memo((_props: any) => {
|
|
594
|
+
return null;
|
|
595
|
+
});
|
|
596
|
+
CustomMonthCaptionWrapper.displayName = 'CustomMonthCaptionWrapper';
|
|
597
|
+
|
|
445
598
|
// Memoize components to ensure stable references
|
|
446
599
|
const defaultComponents = React.useMemo(() => ({
|
|
447
600
|
Root: CustomRoot,
|
|
448
601
|
Months: CustomMonths,
|
|
449
|
-
Month:
|
|
602
|
+
Month: CustomMonthWithProps,
|
|
450
603
|
MonthGrid: CustomMonthGrid,
|
|
451
|
-
// MonthCaption is
|
|
604
|
+
// MonthCaption returns null - actual caption is rendered in CustomMonth inside <caption>
|
|
605
|
+
MonthCaption: CustomMonthCaptionWrapper,
|
|
452
606
|
Weekdays: CustomWeekdays,
|
|
453
607
|
// Spread user components AFTER ours so ours take precedence
|
|
454
608
|
...(components || {}),
|
|
455
|
-
}), [components, CustomRoot, CustomMonths,
|
|
609
|
+
}), [components, CustomRoot, CustomMonths, CustomMonthWithProps, CustomMonthCaptionWrapper, CustomWeekdays]);
|
|
456
610
|
|
|
457
611
|
return (
|
|
458
612
|
<DayPicker
|
|
@@ -474,8 +474,9 @@ describe('Checkbox Component', () => {
|
|
|
474
474
|
const endTime = performance.now();
|
|
475
475
|
|
|
476
476
|
// Performance test: verify rendering completes in reasonable time
|
|
477
|
+
// Note: Performance can vary based on system load, so we use a more lenient threshold
|
|
477
478
|
expect(screen.getAllByRole('checkbox')).toHaveLength(100);
|
|
478
|
-
expect(endTime - startTime).toBeLessThan(
|
|
479
|
+
expect(endTime - startTime).toBeLessThan(3000);
|
|
479
480
|
});
|
|
480
481
|
});
|
|
481
482
|
});
|