@12-apps/ui 1.0.0 → 1.3.1
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/components-catalog.md +14 -0
- package/package.json +42 -27
- package/src/__tests__/data-grid.test.tsx +125 -0
- package/src/__tests__/package-wiring.test.ts +12 -8
- package/src/__tests__/social-login-button.test.tsx +5 -5
- package/src/__tests__/viewport.test.ts +50 -0
- package/src/charts/index.ts +8 -0
- package/src/components/data-display/Alert/Alert.md +1 -1
- package/src/components/data-display/Alert/Alert.stories.tsx +5 -7
- package/src/components/data-display/Alert/Alert.styles.tsx +255 -0
- package/src/components/data-display/Alert/Alert.test.stories.tsx +11 -13
- package/src/components/data-display/Alert/Alert.tsx +135 -328
- package/src/components/data-display/AlertDialog/AlertDialog.stories.tsx +3 -1
- package/src/components/data-display/AlertDialog/AlertDialog.tsx +191 -78
- package/src/components/data-display/AlertDialog/AlertDialog.types.ts +8 -1
- package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.test.stories.tsx +5 -5
- package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.tsx +44 -35
- package/src/components/data-display/Avatar/Avatar.stories.tsx +4 -1
- package/src/components/data-display/Avatar/Avatar.test.stories.tsx +5 -3
- package/src/components/data-display/Avatar/Avatar.tsx +154 -450
- package/src/components/data-display/Avatar/Avatar.view.tsx +396 -0
- package/src/components/data-display/Badge/Badge.animations.ts +67 -0
- package/src/components/data-display/Badge/Badge.helpers.ts +74 -0
- package/src/components/data-display/Badge/Badge.stories.tsx +14 -16
- package/src/components/data-display/Badge/Badge.styles.ts +261 -0
- package/src/components/data-display/Badge/Badge.test.stories.tsx +32 -31
- package/src/components/data-display/Badge/Badge.tsx +151 -557
- package/src/components/data-display/Badge/Badge.variants.ts +106 -0
- package/src/components/data-display/Badge/BadgeContent.tsx +107 -0
- package/src/components/data-display/Banner/Banner.stories.tsx +5 -1
- package/src/components/data-display/Banner/Banner.styles.ts +120 -0
- package/src/components/data-display/Banner/Banner.tsx +179 -224
- package/src/components/data-display/Chart/Chart.md +48 -1
- package/src/components/data-display/Chart/Chart.tsx +82 -500
- package/src/components/data-display/Chart/Chart.types.ts +2 -0
- package/src/components/data-display/Chart/ChartComposables.tsx +121 -0
- package/src/components/data-display/Chart/ChartSpec.types.ts +70 -0
- package/src/components/data-display/Chart/SpecChart.stories.tsx +150 -0
- package/src/components/data-display/Chart/SpecChart.test.stories.tsx +56 -0
- package/src/components/data-display/Chart/SpecChart.tsx +53 -0
- package/src/components/data-display/Chart/__tests__/SpecChart.test.tsx +60 -0
- package/src/components/data-display/Chart/__tests__/chart-format.test.ts +43 -0
- package/src/components/data-display/Chart/__tests__/chart-spec.test.ts +119 -0
- package/src/components/data-display/Chart/chart-format.ts +26 -0
- package/src/components/data-display/Chart/chart-internals.ts +148 -0
- package/src/components/data-display/Chart/chart-renderers.tsx +338 -0
- package/src/components/data-display/Chart/chart-spec-registry.ts +74 -0
- package/src/components/data-display/Chart/chart-spec.ts +171 -0
- package/src/components/data-display/Chart/index.ts +30 -0
- package/src/components/data-display/Chip/Chip.helpers.tsx +131 -0
- package/src/components/data-display/Chip/Chip.stories.tsx +2 -1
- package/src/components/data-display/Chip/Chip.test.stories.tsx +11 -8
- package/src/components/data-display/Chip/Chip.tsx +31 -94
- package/src/components/data-display/DataGrid/DataGrid.body.tsx +141 -0
- package/src/components/data-display/DataGrid/DataGrid.cells.tsx +238 -0
- package/src/components/data-display/DataGrid/DataGrid.model.ts +315 -0
- package/src/components/data-display/DataGrid/DataGrid.rows.ts +112 -0
- package/src/components/data-display/DataGrid/DataGrid.states.tsx +102 -0
- package/src/components/data-display/DataGrid/DataGrid.test.stories.tsx +6 -12
- package/src/components/data-display/DataGrid/DataGrid.tsx +129 -725
- package/src/components/data-display/DataViews/DataViewsGrid.tsx +187 -0
- package/src/components/data-display/DataViews/DataViewsTableBase.tsx +400 -0
- package/src/components/data-display/DataViews/ManageViewsDialog.tsx +140 -0
- package/src/components/data-display/DataViews/SaveViewModal.tsx +289 -0
- package/src/components/data-display/DataViews/__tests__/DataViewsGrid.test.tsx +271 -0
- package/src/components/data-display/DataViews/__tests__/DataViewsTableBase.test.tsx +134 -0
- package/src/components/data-display/DataViews/__tests__/SaveViewModal.test.tsx +83 -0
- package/src/components/data-display/DataViews/__tests__/data-views-filter.test.ts +115 -0
- package/src/components/data-display/DataViews/__tests__/data-views-preview.test.ts +53 -0
- package/src/components/data-display/DataViews/__tests__/range-pill.test.tsx +114 -0
- package/src/components/data-display/DataViews/base-card.stories.tsx +136 -0
- package/src/components/data-display/DataViews/base-card.tsx +278 -0
- package/src/components/data-display/DataViews/data-views-filter-panel.tsx +184 -0
- package/src/components/data-display/DataViews/data-views-filter.ts +138 -0
- package/src/components/data-display/DataViews/data-views-grid-helpers.tsx +248 -0
- package/src/components/data-display/DataViews/data-views-grid-parts.tsx +400 -0
- package/src/components/data-display/DataViews/data-views-inline-bar.tsx +257 -0
- package/src/components/data-display/DataViews/data-views-layout-context.tsx +176 -0
- package/src/components/data-display/DataViews/data-views-pagination.tsx +31 -0
- package/src/components/data-display/DataViews/data-views-preview.ts +53 -0
- package/src/components/data-display/DataViews/data-views-range-pill.tsx +277 -0
- package/src/components/data-display/DataViews/data-views-table-parts.tsx +150 -0
- package/src/components/data-display/DataViews/data-views-toolbar.tsx +149 -0
- package/src/components/data-display/DataViews/data-views-types.ts +312 -0
- package/src/components/data-display/DataViews/data-views-view-mutations.ts +68 -0
- package/src/components/data-display/DataViews/index.ts +33 -0
- package/src/components/data-display/DataViews/use-data-views-state.ts +349 -0
- package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.hooks.ts +104 -0
- package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.tsx +68 -118
- package/src/components/data-display/DescriptionItem/DescriptionItem.stories.tsx +1 -1
- package/src/components/data-display/DescriptionItem/DescriptionItem.tsx +52 -34
- package/src/components/data-display/EmptyState/EmptyState.test.stories.tsx +6 -6
- package/src/components/data-display/EmptyState/EmptyState.tsx +218 -135
- package/src/components/data-display/ErrorState/ErrorState.test.stories.tsx +2 -2
- package/src/components/data-display/ErrorState/ErrorState.tsx +83 -55
- package/src/components/data-display/HoverCard/HoverCard.stories.tsx +3 -1
- package/src/components/data-display/HoverCard/HoverCard.test.stories.tsx +5 -5
- package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.hooks.ts +99 -0
- package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.styles.ts +87 -0
- package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.tsx +28 -155
- package/src/components/data-display/LazyImage/LazyImage.hooks.ts +203 -0
- package/src/components/data-display/LazyImage/LazyImage.md +1 -1
- package/src/components/data-display/LazyImage/LazyImage.stories.tsx +3 -3
- package/src/components/data-display/LazyImage/LazyImage.tsx +144 -243
- package/src/components/data-display/Lightbox/Lightbox.constants.ts +53 -0
- package/src/components/data-display/Lightbox/Lightbox.helpers.ts +39 -0
- package/src/components/data-display/Lightbox/Lightbox.hooks.ts +259 -0
- package/src/components/data-display/Lightbox/Lightbox.test.stories.tsx +28 -35
- package/src/components/data-display/Lightbox/Lightbox.tsx +98 -658
- package/src/components/data-display/Lightbox/LightboxControls.tsx +152 -0
- package/src/components/data-display/Lightbox/LightboxCurrentMedia.tsx +50 -0
- package/src/components/data-display/Lightbox/LightboxMedia.tsx +76 -0
- package/src/components/data-display/Lightbox/LightboxOverlay.tsx +83 -0
- package/src/components/data-display/Lightbox/LightboxStage.tsx +97 -0
- package/src/components/data-display/Lightbox/LightboxThumbnails.tsx +83 -0
- package/src/components/data-display/Lightbox/useLightbox.ts +80 -0
- package/src/components/data-display/LoadingState/LoadingState.helpers.ts +52 -0
- package/src/components/data-display/LoadingState/LoadingState.tsx +117 -87
- package/src/components/data-display/MapPreview/MapControlBar.tsx +116 -0
- package/src/components/data-display/MapPreview/MapDecorations.tsx +155 -0
- package/src/components/data-display/MapPreview/MapLayers.tsx +70 -0
- package/src/components/data-display/MapPreview/MapMarkers.tsx +84 -0
- package/src/components/data-display/MapPreview/MapOverlays.tsx +68 -0
- package/src/components/data-display/MapPreview/MapPreview.constants.ts +19 -0
- package/src/components/data-display/MapPreview/MapPreview.helpers.ts +63 -0
- package/src/components/data-display/MapPreview/MapPreview.hooks.ts +265 -0
- package/src/components/data-display/MapPreview/MapPreview.test.stories.tsx +14 -10
- package/src/components/data-display/MapPreview/MapPreview.tsx +52 -882
- package/src/components/data-display/MapPreview/MapPreview.types.ts +8 -1
- package/src/components/data-display/MapPreview/MapSearchBar.tsx +64 -0
- package/src/components/data-display/MapPreview/MapTiles.tsx +70 -0
- package/src/components/data-display/MapPreview/StaticMapView.tsx +63 -0
- package/src/components/data-display/MapPreview/mapProjection.ts +52 -0
- package/src/components/data-display/MapPreview/mapSurface.ts +37 -0
- package/src/components/data-display/MapPreview/useMapPreview.ts +62 -0
- package/src/components/data-display/Popover/Popover.test.stories.tsx +13 -19
- package/src/components/data-display/Progress/Progress.helpers.ts +37 -0
- package/src/components/data-display/Progress/Progress.styles.ts +101 -0
- package/src/components/data-display/Progress/Progress.tsx +185 -213
- package/src/components/data-display/SectionOnboarding/SectionOnboarding.stories.tsx +1 -1
- package/src/components/data-display/SectionOnboarding/SectionOnboarding.tsx +1 -1
- package/src/components/data-display/StatCard/StatCard.md +117 -0
- package/src/components/data-display/StatCard/StatCard.stories.tsx +167 -0
- package/src/components/data-display/StatCard/StatCard.test.stories.tsx +240 -0
- package/src/components/data-display/StatCard/StatCard.tsx +236 -0
- package/src/components/data-display/StatCard/StatCard.types.ts +62 -0
- package/src/components/data-display/StatCard/index.ts +7 -0
- package/src/components/data-display/Stepper/Stepper.test.stories.tsx +12 -19
- package/src/components/data-display/Stepper/Stepper.tsx +1 -1
- package/src/components/data-display/Table/Table.helpers.ts +31 -0
- package/src/components/data-display/Table/Table.hooks.ts +161 -0
- package/src/components/data-display/Table/Table.stories.tsx +3 -1
- package/src/components/data-display/Table/Table.styles.ts +223 -0
- package/src/components/data-display/Table/Table.test.stories.tsx +4 -4
- package/src/components/data-display/Table/Table.tsx +291 -748
- package/src/components/data-display/Table/TableParts.hooks.tsx +112 -0
- package/src/components/data-display/Table/TableParts.tsx +298 -0
- package/src/components/data-display/Table/TableStates.tsx +21 -0
- package/src/components/data-display/Timeline/Timeline.stories.tsx +10 -12
- package/src/components/data-display/Timeline/Timeline.test.stories.tsx +30 -44
- package/src/components/data-display/Timeline/Timeline.tsx +23 -284
- package/src/components/data-display/Timeline/TimelineRow.tsx +346 -0
- package/src/components/data-display/TimingDiagram/TimingDiagram.tsx +38 -311
- package/src/components/data-display/TimingDiagram/TimingDiagram.views.tsx +346 -0
- package/src/components/data-display/Tooltip/Tooltip.stories.tsx +4 -1
- package/src/components/data-display/Tooltip/Tooltip.test.stories.tsx +44 -58
- package/src/components/data-display/Tooltip/Tooltip.tsx +67 -62
- package/src/components/data-display/index.ts +1 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.hooks.ts +96 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.md +140 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.parts.tsx +154 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.stories.tsx +146 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.test.stories.tsx +114 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.tsx +46 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.types.ts +111 -0
- package/src/components/feedback/ConfirmAction/ConfirmButton.tsx +18 -0
- package/src/components/feedback/ConfirmAction/__tests__/confirm-action.test.tsx +288 -0
- package/src/components/feedback/ConfirmAction/index.ts +16 -0
- package/src/components/feedback/ConfirmAction/with-confirmation.tsx +112 -0
- package/src/components/feedback/Dialog/Dialog.styles.ts +4 -2
- package/src/components/feedback/Dialog/Dialog.tsx +26 -1
- package/src/components/feedback/Dialog/__tests__/dialog-content-spacing.test.tsx +66 -0
- package/src/components/feedback/Modal/Modal.md +1 -1
- package/src/components/feedback/Modal/Modal.stories.tsx +2 -2
- package/src/components/feedback/Sonner/Sonner.tsx +104 -288
- package/src/components/feedback/Sonner/Sonner.types.ts +7 -0
- package/src/components/feedback/Sonner/SonnerToast.tsx +216 -0
- package/src/components/feedback/StackedModal/StackedModal.hooks.ts +226 -0
- package/src/components/feedback/StackedModal/StackedModal.parts.tsx +311 -0
- package/src/components/feedback/StackedModal/StackedModal.stories.tsx +13 -15
- package/src/components/feedback/StackedModal/StackedModal.styles.ts +193 -0
- package/src/components/feedback/StackedModal/StackedModal.tsx +100 -619
- package/src/components/feedback/StackedModal/StackedModal.types.ts +17 -3
- package/src/components/feedback/StackedModal/__tests__/stacked-modal-viewport.test.tsx +104 -0
- package/src/components/feedback/StackedModal/modal-stack.tsx +119 -0
- package/src/components/feedback/Toast/Toast.tsx +92 -81
- package/src/components/feedback/TutorialOverlay/TutorialOverlay.geometry.ts +65 -0
- package/src/components/feedback/TutorialOverlay/TutorialOverlay.hooks.ts +185 -0
- package/src/components/feedback/TutorialOverlay/TutorialOverlay.test.stories.tsx +2 -2
- package/src/components/feedback/TutorialOverlay/TutorialOverlay.tsx +156 -414
- package/src/components/feedback/TutorialOverlay/TutorialStepBody.tsx +255 -0
- package/src/components/feedback/index.ts +1 -0
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.helpers.ts +58 -0
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.hooks.ts +93 -0
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.mapper.ts +57 -0
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.stories.tsx +4 -2
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.test.stories.tsx +20 -16
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.tsx +26 -829
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.types.ts +11 -0
- package/src/components/form/AddressAutocomplete/AddressAutocompleteField.tsx +86 -0
- package/src/components/form/AddressAutocomplete/AddressFallbackField.tsx +64 -0
- package/src/components/form/AddressAutocomplete/AddressInputField.tsx +80 -0
- package/src/components/form/AddressAutocomplete/AddressOption.tsx +83 -0
- package/src/components/form/AddressAutocomplete/AddressSuggestionsPaper.tsx +24 -0
- package/src/components/form/AddressAutocomplete/AddressTextField.tsx +43 -0
- package/src/components/form/AddressAutocomplete/mockPlaces.ts +208 -0
- package/src/components/form/AddressAutocomplete/useAddressPredictions.ts +136 -0
- package/src/components/form/AddressAutocomplete/useAddressSelection.ts +113 -0
- package/src/components/form/AddressAutocomplete/useCurrentLocation.ts +116 -0
- package/src/components/form/AddressAutocomplete/useGoogleMapsAPI.ts +75 -0
- package/src/components/form/Button/Button.helpers.ts +36 -0
- package/src/components/form/Button/Button.stories.tsx +4 -1
- package/src/components/form/Button/Button.styles.ts +225 -0
- package/src/components/form/Button/Button.test.stories.tsx +25 -21
- package/src/components/form/Button/Button.tsx +45 -252
- package/src/components/form/Calendar/Calendar.dates.ts +100 -0
- package/src/components/form/Calendar/Calendar.helpers.ts +32 -0
- package/src/components/form/Calendar/Calendar.hooks.ts +278 -0
- package/src/components/form/Calendar/Calendar.keyboard.ts +32 -0
- package/src/components/form/Calendar/Calendar.selection.ts +156 -0
- package/src/components/form/Calendar/Calendar.test.stories.tsx +33 -34
- package/src/components/form/Calendar/Calendar.tsx +63 -551
- package/src/components/form/Calendar/Calendar.types.ts +16 -0
- package/src/components/form/Calendar/CalendarDay.tsx +127 -0
- package/src/components/form/Calendar/CalendarDayCell.tsx +52 -0
- package/src/components/form/Calendar/CalendarMonth.tsx +128 -0
- package/src/components/form/CepField/CepField.md +138 -0
- package/src/components/form/CepField/CepField.stories.tsx +138 -0
- package/src/components/form/CepField/CepField.test.stories.tsx +281 -0
- package/src/components/form/CepField/CepField.tsx +211 -0
- package/src/components/form/CepField/CepField.types.ts +73 -0
- package/src/components/form/CepField/index.ts +2 -0
- package/src/components/form/Checkbox/Checkbox.helpers.ts +28 -0
- package/src/components/form/Checkbox/Checkbox.tsx +19 -16
- package/src/components/form/CodeEditor/CodeEditor.helpers.ts +45 -0
- package/src/components/form/CodeEditor/CodeEditor.hooks.ts +104 -0
- package/src/components/form/CodeEditor/CodeEditor.monaco.ts +109 -0
- package/src/components/form/CodeEditor/CodeEditor.test.stories.tsx +17 -18
- package/src/components/form/CodeEditor/CodeEditor.tsx +81 -330
- package/src/components/form/CodeEditor/CodeEditorToolbar.tsx +168 -0
- package/src/components/form/Command/Command.hooks.ts +207 -0
- package/src/components/form/Command/Command.parts.tsx +58 -0
- package/src/components/form/Command/Command.styles.ts +83 -0
- package/src/components/form/Command/Command.tsx +61 -270
- package/src/components/form/CreatableSelect/CreatableSelect.md +1 -1
- package/src/components/form/CreatableSelect/CreatableSelect.test.stories.tsx +1 -1
- package/src/components/form/CreatableSelect/CreatableSelect.tsx +152 -71
- package/src/components/form/CreatableSelect/CreatableSelect.types.ts +13 -0
- package/src/components/form/HeaderButton/HeaderButton.tsx +80 -0
- package/src/components/form/HeaderButton/index.ts +2 -0
- package/src/components/form/Input/Input.stories.tsx +5 -1
- package/src/components/form/Input/Input.styles.ts +176 -0
- package/src/components/form/Input/Input.test.stories.tsx +30 -23
- package/src/components/form/Input/Input.tsx +71 -211
- package/src/components/form/Input/Input.types.ts +11 -0
- package/src/components/form/InputOTP/InputOTP.test.stories.tsx +9 -17
- package/src/components/form/Menubar/Menubar.test.stories.tsx +4 -8
- package/src/components/form/PasswordStrength/PasswordStrength.helpers.tsx +117 -0
- package/src/components/form/PasswordStrength/PasswordStrength.test.stories.tsx +3 -3
- package/src/components/form/PasswordStrength/PasswordStrength.tsx +202 -255
- package/src/components/form/PhoneInput/PhoneInput.hooks.ts +134 -0
- package/src/components/form/PhoneInput/PhoneInput.test.stories.tsx +4 -4
- package/src/components/form/PhoneInput/PhoneInput.tsx +279 -292
- package/src/components/form/PhoneInput/countries.ts +54 -0
- package/src/components/form/PhoneInput/phone.ts +47 -0
- package/src/components/form/RadioGroup/RadioGroup.test.stories.tsx +9 -9
- package/src/components/form/RichTextEditor/RichTextEditor.dom.ts +124 -0
- package/src/components/form/RichTextEditor/RichTextEditor.format.ts +61 -0
- package/src/components/form/RichTextEditor/RichTextEditor.hooks.ts +142 -0
- package/src/components/form/RichTextEditor/RichTextEditor.test.stories.tsx +5 -12
- package/src/components/form/RichTextEditor/RichTextEditor.tsx +98 -410
- package/src/components/form/RichTextEditor/RichTextEditorToolbar.tsx +187 -0
- package/src/components/form/SearchPalette/SearchPalette.parts.tsx +22 -0
- package/src/components/form/SearchPalette/SearchPalette.tsx +13 -9
- package/src/components/form/Select/Select.test.stories.tsx +35 -27
- package/src/components/form/Slider/Slider.test.stories.tsx +9 -11
- package/src/components/form/Switch/Switch.md +2 -1
- package/src/components/form/Switch/Switch.test.stories.tsx +6 -6
- package/src/components/form/Textarea/Textarea.styles.ts +143 -0
- package/src/components/form/Textarea/Textarea.test.stories.tsx +20 -26
- package/src/components/form/Textarea/Textarea.tsx +114 -535
- package/src/components/form/Textarea/TextareaRichEditor.tsx +377 -0
- package/src/components/form/Toggle/Toggle.styles.ts +211 -0
- package/src/components/form/Toggle/Toggle.test.stories.tsx +19 -27
- package/src/components/form/Toggle/Toggle.tsx +10 -185
- package/src/components/form/UploadButton/UploadButton.hooks.ts +323 -0
- package/src/components/form/UploadButton/UploadButton.parts.tsx +210 -0
- package/src/components/form/UploadButton/UploadButton.stories.tsx +2 -1
- package/src/components/form/UploadButton/UploadButton.test.stories.tsx +123 -4
- package/src/components/form/UploadButton/UploadButton.tsx +76 -305
- package/src/components/form/UploadButton/UploadButton.types.ts +16 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/form/total-form/Fields/CepField.tsx +143 -0
- package/src/components/form/total-form/Fields/ToggleField.tsx +61 -0
- package/src/components/form/total-form/Fields/index.ts +4 -0
- package/src/components/form/total-form/FormContainer.tsx +3 -3
- package/src/components/form/total-form/SubmitButton.tsx +25 -2
- package/src/components/form/total-form/total-form.test.tsx +118 -1
- package/src/components/layout/Accordion/Accordion.md +1 -1
- package/src/components/layout/Accordion/Accordion.stories.tsx +5 -1
- package/src/components/layout/Accordion/Accordion.styles.ts +80 -0
- package/src/components/layout/Accordion/Accordion.test.stories.tsx +32 -50
- package/src/components/layout/Accordion/Accordion.tsx +2 -75
- package/src/components/layout/Card/Card.helpers.ts +30 -0
- package/src/components/layout/Card/Card.stories.tsx +9 -11
- package/src/components/layout/Card/Card.styles.ts +182 -0
- package/src/components/layout/Card/Card.test.stories.tsx +46 -46
- package/src/components/layout/Card/Card.tsx +24 -241
- package/src/components/layout/CardGrid/CardGrid.md +96 -0
- package/src/components/layout/CardGrid/CardGrid.tsx +140 -0
- package/src/components/layout/CardGrid/__tests__/CardGrid.test.tsx +92 -0
- package/src/components/layout/CardGrid/index.ts +2 -0
- package/src/components/layout/Collapsible/Collapsible.helpers.ts +83 -0
- package/src/components/layout/Collapsible/Collapsible.test.stories.tsx +7 -3
- package/src/components/layout/Collapsible/Collapsible.tsx +111 -107
- package/src/components/layout/Container/Container.test.stories.tsx +11 -16
- package/src/components/layout/ContentToolbar/ColumnsMenu.tsx +1 -1
- package/src/components/layout/ContentToolbar/ContentToolbar.tsx +120 -44
- package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +9 -0
- package/src/components/layout/ContentToolbar/FilterTrigger.tsx +1 -1
- package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +58 -43
- package/src/components/layout/ContentToolbar/MultiSelectTriggers.tsx +139 -0
- package/src/components/layout/ContentToolbar/SavedViewsMenu.tsx +10 -3
- package/src/components/layout/ContentToolbar/SortByDropdown.tsx +35 -11
- package/src/components/layout/ContentToolbar/ViewSelector.tsx +5 -6
- package/src/components/layout/ContentToolbar/__tests__/MultiSelectDropdown.test.tsx +21 -0
- package/src/components/layout/Dashboard/Dashboard.test.stories.tsx +27 -1
- package/src/components/layout/Dashboard/Dashboard.tsx +27 -8
- package/src/components/layout/Dashboard/DashboardParts.tsx +12 -19
- package/src/components/layout/Dashboard/index.ts +2 -1
- package/src/components/layout/Drawer/Drawer.stories.tsx +8 -10
- package/src/components/layout/Drawer/Drawer.test.stories.tsx +18 -14
- package/src/components/layout/Drawer/Drawer.tsx +67 -53
- package/src/components/layout/Resizable/Resizable.helpers.ts +133 -0
- package/src/components/layout/Resizable/Resizable.hooks.ts +68 -0
- package/src/components/layout/Resizable/Resizable.tsx +38 -153
- package/src/components/layout/ScrollArea/ScrollArea.helpers.ts +51 -0
- package/src/components/layout/ScrollArea/ScrollArea.styles.ts +248 -0
- package/src/components/layout/ScrollArea/ScrollArea.tsx +183 -374
- package/src/components/layout/ScrollArea/ScrollAreaContent.tsx +70 -0
- package/src/components/layout/ScrollArea/ScrollViewport.tsx +90 -0
- package/src/components/layout/Separator/Separator.styles.ts +128 -0
- package/src/components/layout/Separator/Separator.test.stories.tsx +3 -12
- package/src/components/layout/Separator/Separator.tsx +6 -132
- package/src/components/layout/SettingsLayout/SettingsLayout.parts.tsx +2 -1
- package/src/components/layout/SettingsLayout/SettingsLayout.stories.tsx +6 -8
- package/src/components/layout/Sidebar/Sidebar.stories.tsx +10 -12
- package/src/components/layout/Sidebar/Sidebar.test.stories.tsx +35 -37
- package/src/components/layout/Skeleton/Skeleton.helpers.ts +33 -0
- package/src/components/layout/Skeleton/Skeleton.styles.ts +114 -0
- package/src/components/layout/Skeleton/Skeleton.test.stories.tsx +3 -3
- package/src/components/layout/Skeleton/Skeleton.tsx +67 -155
- package/src/components/layout/TableFilter/TableFilter.tsx +2 -1
- package/src/components/navigation/Breadcrumbs/BreadcrumbEntry.tsx +305 -0
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.md +2 -1
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.stories.tsx +12 -14
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.styles.tsx +212 -0
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.test.stories.tsx +62 -61
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +116 -429
- package/src/components/navigation/CommandPalette/CommandPalette.helpers.ts +82 -0
- package/src/components/navigation/CommandPalette/CommandPalette.hooks.ts +202 -0
- package/src/components/navigation/CommandPalette/CommandPalette.stories.tsx +3 -1
- package/src/components/navigation/CommandPalette/CommandPalette.test.stories.tsx +3 -5
- package/src/components/navigation/CommandPalette/CommandPalette.tsx +108 -341
- package/src/components/navigation/CommandPalette/CommandPaletteResults.tsx +182 -0
- package/src/components/navigation/CommandPalette/ShortcutChip.tsx +18 -0
- package/src/components/navigation/ContextMenu/ContextMenu.md +3 -1
- package/src/components/navigation/ContextMenu/ContextMenu.stories.tsx +4 -1
- package/src/components/navigation/ContextMenu/ContextMenu.test.stories.tsx +19 -24
- package/src/components/navigation/ContextMenu/ContextMenu.tsx +70 -54
- package/src/components/navigation/DropdownMenu/DropdownMenu.md +3 -1
- package/src/components/navigation/DropdownMenu/DropdownMenu.stories.tsx +17 -19
- package/src/components/navigation/DropdownMenu/DropdownMenu.test.stories.tsx +26 -26
- package/src/components/navigation/DropdownMenu/DropdownMenu.tsx +189 -111
- package/src/components/navigation/NavigationMenu/NavigationMegaSection.tsx +66 -0
- package/src/components/navigation/NavigationMenu/NavigationMenu.md +7 -2
- package/src/components/navigation/NavigationMenu/NavigationMenu.shell.tsx +218 -0
- package/src/components/navigation/NavigationMenu/NavigationMenu.styles.ts +223 -0
- package/src/components/navigation/NavigationMenu/NavigationMenu.tsx +119 -808
- package/src/components/navigation/NavigationMenu/NavigationMenuItem.hooks.ts +126 -0
- package/src/components/navigation/NavigationMenu/NavigationMenuItem.tsx +352 -0
- package/src/components/navigation/NavigationMenu/NavigationStandardBody.tsx +148 -0
- package/src/components/navigation/NavigationMenu/stories/NavigationMenu.stories.tsx +15 -17
- package/src/components/navigation/NavigationMenu/stories/NavigationMenu.test.stories.tsx +27 -23
- package/src/components/navigation/NavigationMenu/stories/NavigationMenuHorizontal.stories.tsx +11 -13
- package/src/components/navigation/NavigationMenu/stories/NavigationMenuVertical.stories.tsx +9 -11
- package/src/components/navigation/Pagination/Pagination.helpers.tsx +85 -0
- package/src/components/navigation/Pagination/Pagination.md +2 -1
- package/src/components/navigation/Pagination/Pagination.stories.tsx +4 -6
- package/src/components/navigation/Pagination/Pagination.styles.ts +135 -0
- package/src/components/navigation/Pagination/Pagination.tsx +178 -313
- package/src/components/navigation/Pagination/PaginationParts.tsx +81 -0
- package/src/components/navigation/Tabs/Tabs.md +3 -1
- package/src/components/navigation/Tabs/Tabs.stories.tsx +21 -23
- package/src/components/navigation/Tabs/Tabs.styles.ts +163 -0
- package/src/components/navigation/Tabs/Tabs.test.stories.tsx +37 -37
- package/src/components/navigation/Tabs/Tabs.tsx +192 -361
- package/src/components/navigation/Tabs/TabsPanel.tsx +100 -0
- package/src/components/typography/Blockquote/Blockquote.tsx +1 -1
- package/src/components/typography/Code/Code.test.stories.tsx +5 -5
- package/src/components/typography/Code/Code.tsx +74 -49
- package/src/components/typography/Heading/Heading.test.stories.tsx +8 -8
- package/src/components/typography/Paragraph/Paragraph.test.stories.tsx +5 -12
- package/src/components/typography/Text/Text.tsx +84 -77
- package/src/components/utility/AnimatedIcon/AnimatedIcon.md +4 -4
- package/src/components/utility/AnimatedIcon/AnimatedIcon.stories.tsx +19 -21
- package/src/components/utility/AnimatedIcon/AnimatedIcon.test.stories.tsx +19 -12
- package/src/components/utility/AspectRatio/AspectRatio.test.stories.tsx +9 -9
- package/src/components/utility/InfiniteScroll/InfiniteScroll.hooks.ts +118 -0
- package/src/components/utility/InfiniteScroll/InfiniteScroll.parts.tsx +54 -0
- package/src/components/utility/InfiniteScroll/InfiniteScroll.test.stories.tsx +12 -9
- package/src/components/utility/InfiniteScroll/InfiniteScroll.tsx +29 -148
- package/src/components/utility/InstallPrompt/InstallPrompt.helpers.ts +124 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.md +113 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.stories.tsx +118 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.styles.ts +50 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.test.stories.tsx +83 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.tsx +192 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.types.ts +78 -0
- package/src/components/utility/InstallPrompt/__tests__/InstallPrompt.ios.test.tsx +65 -0
- package/src/components/utility/InstallPrompt/__tests__/usePwaInstall.test.tsx +271 -0
- package/src/components/utility/InstallPrompt/index.ts +10 -0
- package/src/components/utility/InstallPrompt/usePwaInstall.ts +171 -0
- package/src/components/utility/Transition/Transition.helpers.ts +40 -0
- package/src/components/utility/Transition/Transition.tsx +17 -68
- package/src/components/utility/VirtualList/VirtualList.hooks.ts +319 -0
- package/src/components/utility/VirtualList/VirtualList.test.stories.tsx +49 -38
- package/src/components/utility/VirtualList/VirtualList.tsx +110 -315
- package/src/components/utility/WorkflowStep/WorkflowStep.helpers.ts +151 -0
- package/src/components/utility/WorkflowStep/WorkflowStep.parts.tsx +371 -0
- package/src/components/utility/WorkflowStep/WorkflowStep.stories.tsx +9 -11
- package/src/components/utility/WorkflowStep/WorkflowStep.test.stories.tsx +7 -7
- package/src/components/utility/WorkflowStep/WorkflowStep.tsx +51 -449
- package/src/components/utility/WorkflowStep/WorkflowStepRow.tsx +96 -0
- package/src/components/utility/index.ts +1 -0
- package/src/mui/Link.ts +2 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/viewport.ts +65 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
|
4
|
+
|
|
5
|
+
import { CepField } from './CepField';
|
|
6
|
+
import type { CepAddress } from './CepField.types';
|
|
7
|
+
|
|
8
|
+
const ADDRESS: CepAddress = {
|
|
9
|
+
cep: '01310100',
|
|
10
|
+
street: 'Avenida Paulista',
|
|
11
|
+
neighborhood: 'Bela Vista',
|
|
12
|
+
city: 'São Paulo',
|
|
13
|
+
state: 'SP',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const meta: Meta<typeof CepField> = {
|
|
17
|
+
title: 'Form/CepField/Tests',
|
|
18
|
+
component: CepField,
|
|
19
|
+
parameters: {
|
|
20
|
+
layout: 'centered',
|
|
21
|
+
chromatic: { disableSnapshot: false },
|
|
22
|
+
docs: {
|
|
23
|
+
description: { component: 'Test stories for CepField masking, lookup and race-safety' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
tags: ['autodocs', 'test'],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
export type Story = StoryObj<typeof meta>;
|
|
31
|
+
|
|
32
|
+
/** Controlled harness — the field is controlled, so tests need real state. */
|
|
33
|
+
function Harness({
|
|
34
|
+
onLookup,
|
|
35
|
+
onResolved,
|
|
36
|
+
onNotFound,
|
|
37
|
+
debounceMs = 0,
|
|
38
|
+
initial = '',
|
|
39
|
+
disabled = false,
|
|
40
|
+
}: {
|
|
41
|
+
onLookup?: (cep: string) => Promise<CepAddress | null>;
|
|
42
|
+
onResolved?: (address: CepAddress) => void;
|
|
43
|
+
onNotFound?: () => void;
|
|
44
|
+
debounceMs?: number;
|
|
45
|
+
initial?: string;
|
|
46
|
+
disabled?: boolean;
|
|
47
|
+
}): React.ReactElement {
|
|
48
|
+
const [value, setValue] = useState(initial);
|
|
49
|
+
return (
|
|
50
|
+
<CepField
|
|
51
|
+
value={value}
|
|
52
|
+
onChange={setValue}
|
|
53
|
+
onLookup={onLookup}
|
|
54
|
+
onResolved={onResolved}
|
|
55
|
+
onNotFound={onNotFound}
|
|
56
|
+
debounceMs={debounceMs}
|
|
57
|
+
disabled={disabled}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 1. Masking
|
|
63
|
+
|
|
64
|
+
export const MasksWhileTyping: Story = {
|
|
65
|
+
render: () => <Harness />,
|
|
66
|
+
play: async ({ canvasElement }) => {
|
|
67
|
+
const canvas = within(canvasElement);
|
|
68
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
69
|
+
|
|
70
|
+
await userEvent.type(input, '01310100');
|
|
71
|
+
await expect(input).toHaveValue('01310-100');
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const StripsNonDigitsAndCaps: Story = {
|
|
76
|
+
render: () => <Harness />,
|
|
77
|
+
play: async ({ canvasElement }) => {
|
|
78
|
+
const canvas = within(canvasElement);
|
|
79
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
80
|
+
|
|
81
|
+
await userEvent.type(input, 'ab01310100999');
|
|
82
|
+
await expect(input).toHaveValue('01310-100');
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 2. Lookup
|
|
87
|
+
|
|
88
|
+
export const ResolvesACompleteCep: Story = {
|
|
89
|
+
render: function ResolvesStory() {
|
|
90
|
+
const onResolved = fn();
|
|
91
|
+
return (
|
|
92
|
+
<Harness onLookup={async () => ADDRESS} onResolved={onResolved} />
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
play: async ({ canvasElement }) => {
|
|
96
|
+
const canvas = within(canvasElement);
|
|
97
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
98
|
+
|
|
99
|
+
await userEvent.type(input, '01310100');
|
|
100
|
+
await waitFor(() => expect(canvas.getByTestId('cep-field-postalCode-found')).toBeInTheDocument());
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const DoesNotLookUpAnIncompleteCep: Story = {
|
|
105
|
+
render: function IncompleteStory() {
|
|
106
|
+
const lookup = fn(async () => ADDRESS);
|
|
107
|
+
return <Harness onLookup={lookup} />;
|
|
108
|
+
},
|
|
109
|
+
play: async ({ canvasElement }) => {
|
|
110
|
+
const canvas = within(canvasElement);
|
|
111
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
112
|
+
|
|
113
|
+
await userEvent.type(input, '0131');
|
|
114
|
+
// Presence evidence first: the field really did take the partial value…
|
|
115
|
+
await expect(input).toHaveValue('0131');
|
|
116
|
+
// …and no status line ever appears, because an incomplete CEP is never
|
|
117
|
+
// queried. waitFor gives the (absent) debounce a chance to fire first.
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(canvas.queryByTestId('cep-field-postalCode-loading')).toBeNull();
|
|
120
|
+
expect(canvas.queryByTestId('cep-field-postalCode-found')).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const ReportsAnUnknownCep: Story = {
|
|
126
|
+
render: () => <Harness onLookup={async () => null} />,
|
|
127
|
+
play: async ({ canvasElement }) => {
|
|
128
|
+
const canvas = within(canvasElement);
|
|
129
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
130
|
+
|
|
131
|
+
await userEvent.type(input, '00000000');
|
|
132
|
+
await waitFor(() =>
|
|
133
|
+
expect(canvas.getByTestId('cep-field-postalCode-notfound')).toBeInTheDocument(),
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A miss must be REPORTED, not just displayed: callers that auto-filled from a
|
|
140
|
+
* previous CEP rely on `onNotFound` to undo it, so the form never shows one
|
|
141
|
+
* CEP's street under another's.
|
|
142
|
+
*/
|
|
143
|
+
export const ReportsAMissToTheCaller: Story = {
|
|
144
|
+
render: function ReportsMissStory() {
|
|
145
|
+
const [missed, setMissed] = useState(false);
|
|
146
|
+
return (
|
|
147
|
+
<div>
|
|
148
|
+
<Harness onLookup={async () => null} onNotFound={() => setMissed(true)} />
|
|
149
|
+
<span data-testid="miss-flag">{missed ? 'missed' : 'clean'}</span>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
play: async ({ canvasElement }) => {
|
|
154
|
+
const canvas = within(canvasElement);
|
|
155
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
156
|
+
|
|
157
|
+
await userEvent.type(input, '00000000');
|
|
158
|
+
await waitFor(() => expect(canvas.getByTestId('miss-flag')).toHaveTextContent('missed'));
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** …and a resolved CEP must NOT report a miss. */
|
|
163
|
+
export const DoesNotReportAMissOnSuccess: Story = {
|
|
164
|
+
render: function NoMissStory() {
|
|
165
|
+
const [missed, setMissed] = useState(false);
|
|
166
|
+
return (
|
|
167
|
+
<div>
|
|
168
|
+
<Harness onLookup={async () => ADDRESS} onNotFound={() => setMissed(true)} />
|
|
169
|
+
<span data-testid="miss-flag">{missed ? 'missed' : 'clean'}</span>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
play: async ({ canvasElement }) => {
|
|
174
|
+
const canvas = within(canvasElement);
|
|
175
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
176
|
+
|
|
177
|
+
await userEvent.type(input, '01310100');
|
|
178
|
+
await waitFor(() => expect(canvas.getByTestId('cep-field-postalCode-found')).toBeInTheDocument());
|
|
179
|
+
await expect(canvas.getByTestId('miss-flag')).toHaveTextContent('clean');
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** A lookup that throws is indistinguishable from "not found" — never a crash. */
|
|
184
|
+
export const SurvivesAThrowingLookup: Story = {
|
|
185
|
+
render: () => (
|
|
186
|
+
<Harness
|
|
187
|
+
onLookup={async () => {
|
|
188
|
+
throw new Error('provider down');
|
|
189
|
+
}}
|
|
190
|
+
/>
|
|
191
|
+
),
|
|
192
|
+
play: async ({ canvasElement }) => {
|
|
193
|
+
const canvas = within(canvasElement);
|
|
194
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
195
|
+
|
|
196
|
+
await userEvent.type(input, '01310100');
|
|
197
|
+
await waitFor(() =>
|
|
198
|
+
expect(canvas.getByTestId('cep-field-postalCode-notfound')).toBeInTheDocument(),
|
|
199
|
+
);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* …including a lookup that throws BEFORE returning a promise, which is what a
|
|
205
|
+
* misconfigured adapter does. Awaiting the call and catching on its result
|
|
206
|
+
* would miss this one entirely and surface it as an unhandled rejection.
|
|
207
|
+
*/
|
|
208
|
+
export const SurvivesASynchronousThrow: Story = {
|
|
209
|
+
render: () => (
|
|
210
|
+
<Harness
|
|
211
|
+
onLookup={() => {
|
|
212
|
+
throw new Error('adapter misconfigured');
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
),
|
|
216
|
+
play: async ({ canvasElement }) => {
|
|
217
|
+
const canvas = within(canvasElement);
|
|
218
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
219
|
+
|
|
220
|
+
await userEvent.type(input, '01310100');
|
|
221
|
+
await waitFor(() =>
|
|
222
|
+
expect(canvas.getByTestId('cep-field-postalCode-notfound')).toBeInTheDocument(),
|
|
223
|
+
);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// 3. State
|
|
228
|
+
|
|
229
|
+
export const DisabledSuppressesLookup: Story = {
|
|
230
|
+
render: () => <Harness initial="01310-100" disabled onLookup={async () => ADDRESS} />,
|
|
231
|
+
play: async ({ canvasElement }) => {
|
|
232
|
+
const canvas = within(canvasElement);
|
|
233
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
234
|
+
|
|
235
|
+
await expect(input).toBeDisabled();
|
|
236
|
+
await expect(input).toHaveValue('01310-100');
|
|
237
|
+
// A complete CEP, but disabled suppresses the lookup entirely.
|
|
238
|
+
await waitFor(() => expect(canvas.queryByTestId('cep-field-postalCode-found')).toBeNull());
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export const ClearsStatusWhenEditedBackToIncomplete: Story = {
|
|
243
|
+
render: () => <Harness onLookup={async () => ADDRESS} />,
|
|
244
|
+
play: async ({ canvasElement }) => {
|
|
245
|
+
const canvas = within(canvasElement);
|
|
246
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
247
|
+
|
|
248
|
+
await userEvent.type(input, '01310100');
|
|
249
|
+
await waitFor(() => expect(canvas.getByTestId('cep-field-postalCode-found')).toBeInTheDocument());
|
|
250
|
+
|
|
251
|
+
await userEvent.type(input, '{backspace}');
|
|
252
|
+
await waitFor(() => expect(canvas.queryByTestId('cep-field-postalCode-found')).toBeNull());
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// 4. Accessibility
|
|
257
|
+
|
|
258
|
+
export const ExposesAccessibleStatus: Story = {
|
|
259
|
+
render: () => <Harness onLookup={async () => ADDRESS} />,
|
|
260
|
+
play: async ({ canvasElement }) => {
|
|
261
|
+
const canvas = within(canvasElement);
|
|
262
|
+
const input = canvas.getByTestId('cep-field-postalCode');
|
|
263
|
+
|
|
264
|
+
await expect(input).toHaveAttribute('aria-describedby', 'cep-field-postalCode-status');
|
|
265
|
+
await userEvent.type(input, '01310100');
|
|
266
|
+
await waitFor(() => expect(canvas.getByRole('status')).toBeInTheDocument());
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export const RendersACallerError: Story = {
|
|
271
|
+
render: function ErrorStory() {
|
|
272
|
+
const [value, setValue] = useState('123');
|
|
273
|
+
return <CepField value={value} onChange={setValue} error="CEP inválido." />;
|
|
274
|
+
},
|
|
275
|
+
play: async ({ canvasElement }) => {
|
|
276
|
+
const canvas = within(canvasElement);
|
|
277
|
+
await expect(canvas.getByTestId('cep-field-postalCode-message')).toHaveTextContent(
|
|
278
|
+
'CEP inválido.',
|
|
279
|
+
);
|
|
280
|
+
},
|
|
281
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { cepDigits, formatCep, isValidCep } from '@12-apps/forms-core';
|
|
6
|
+
|
|
7
|
+
import { FormControl, FormLabel, FormMessage } from '../Form';
|
|
8
|
+
import { Input } from '../Input';
|
|
9
|
+
import type { CepAddress, CepFieldProps, CepLookupStatus } from './CepField.types';
|
|
10
|
+
|
|
11
|
+
/** What the user is told for each non-idle outcome. */
|
|
12
|
+
const STATUS_TEXT: Record<Exclude<CepLookupStatus, 'idle'>, string> = {
|
|
13
|
+
loading: 'Buscando endereço…',
|
|
14
|
+
found: 'Endereço preenchido pelo CEP.',
|
|
15
|
+
notfound: 'CEP não encontrado — preencha o endereço manualmente.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Everything {@link useCepLookup} needs, so the hook signature stays flat. */
|
|
19
|
+
interface LookupOptions {
|
|
20
|
+
digits: string;
|
|
21
|
+
complete: boolean;
|
|
22
|
+
disabled: boolean;
|
|
23
|
+
debounceMs: number;
|
|
24
|
+
onLookup?: (cep: string) => Promise<CepAddress | null>;
|
|
25
|
+
onResolved?: (address: CepAddress) => void;
|
|
26
|
+
onNotFound?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Debounced, race-safe CEP resolution.
|
|
31
|
+
*
|
|
32
|
+
* Two guards earn their keep here: a monotonic sequence id, bumped
|
|
33
|
+
* synchronously on every new query, so a slow response for a since-edited CEP
|
|
34
|
+
* can never land; and a "last queried" marker, so re-rendering (or the caller
|
|
35
|
+
* re-masking the same value) never re-hits the provider.
|
|
36
|
+
*/
|
|
37
|
+
function useCepLookup({
|
|
38
|
+
digits,
|
|
39
|
+
complete,
|
|
40
|
+
disabled,
|
|
41
|
+
debounceMs,
|
|
42
|
+
onLookup,
|
|
43
|
+
onResolved,
|
|
44
|
+
onNotFound,
|
|
45
|
+
}: LookupOptions): CepLookupStatus {
|
|
46
|
+
const [status, setStatus] = useState<CepLookupStatus>('idle');
|
|
47
|
+
const lastQueriedRef = useRef('');
|
|
48
|
+
const seqRef = useRef(0);
|
|
49
|
+
// Latest callbacks, read at call time — keeps them out of the effect deps so
|
|
50
|
+
// an inline arrow from the caller doesn't re-trigger the lookup every render.
|
|
51
|
+
const lookupRef = useRef(onLookup);
|
|
52
|
+
const resolvedRef = useRef(onResolved);
|
|
53
|
+
const notFoundRef = useRef(onNotFound);
|
|
54
|
+
lookupRef.current = onLookup;
|
|
55
|
+
resolvedRef.current = onResolved;
|
|
56
|
+
notFoundRef.current = onNotFound;
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!complete || disabled || !lookupRef.current) {
|
|
60
|
+
// Editing away from a complete CEP drops any stale status and forgets the
|
|
61
|
+
// last query, so re-typing the same CEP resolves again.
|
|
62
|
+
seqRef.current += 1;
|
|
63
|
+
setStatus('idle');
|
|
64
|
+
lastQueriedRef.current = '';
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (lastQueriedRef.current === digits) return;
|
|
68
|
+
|
|
69
|
+
const mySeq = (seqRef.current += 1);
|
|
70
|
+
const handle = setTimeout(() => {
|
|
71
|
+
lastQueriedRef.current = digits;
|
|
72
|
+
setStatus('loading');
|
|
73
|
+
void (async () => {
|
|
74
|
+
// A failing lookup is treated exactly like "not found" — the user types
|
|
75
|
+
// the address instead. Never surfaced as a crash. Going through
|
|
76
|
+
// `Promise.resolve().then` rather than calling directly covers the two
|
|
77
|
+
// failure modes an `await … .catch()` misses: a lookup that throws
|
|
78
|
+
// SYNCHRONOUSLY, and one that returns something other than a promise.
|
|
79
|
+
const address = await Promise.resolve()
|
|
80
|
+
.then(() => lookupRef.current?.(digits))
|
|
81
|
+
.catch(() => null);
|
|
82
|
+
if (seqRef.current !== mySeq) return;
|
|
83
|
+
setStatus(address ? 'found' : 'notfound');
|
|
84
|
+
// A miss is reported too, so a caller that filled from a PREVIOUS CEP
|
|
85
|
+
// can undo it rather than leaving that address under this CEP.
|
|
86
|
+
if (address) resolvedRef.current?.(address);
|
|
87
|
+
else notFoundRef.current?.();
|
|
88
|
+
})();
|
|
89
|
+
}, debounceMs);
|
|
90
|
+
|
|
91
|
+
return () => clearTimeout(handle);
|
|
92
|
+
}, [digits, complete, disabled, debounceMs]);
|
|
93
|
+
|
|
94
|
+
return status;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The lookup outcome as a live region, so screen readers announce it without
|
|
99
|
+
* stealing focus. Silent while idle, and while the caller is showing its own
|
|
100
|
+
* validation error (which would otherwise stack two messages).
|
|
101
|
+
*/
|
|
102
|
+
function CepStatus({
|
|
103
|
+
status,
|
|
104
|
+
testId,
|
|
105
|
+
suppressed,
|
|
106
|
+
}: {
|
|
107
|
+
status: CepLookupStatus;
|
|
108
|
+
testId: string;
|
|
109
|
+
suppressed: boolean;
|
|
110
|
+
}): React.ReactElement {
|
|
111
|
+
const visible = !suppressed && status !== 'idle';
|
|
112
|
+
return (
|
|
113
|
+
<span id={`${testId}-status`} role="status" aria-live="polite">
|
|
114
|
+
{visible && (
|
|
115
|
+
<FormMessage error={status === 'notfound'} dataTestId={`${testId}-${status}`}>
|
|
116
|
+
{STATUS_TEXT[status as Exclude<CepLookupStatus, 'idle'>]}
|
|
117
|
+
</FormMessage>
|
|
118
|
+
)}
|
|
119
|
+
</span>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A CEP input that fills the rest of the address in.
|
|
125
|
+
*
|
|
126
|
+
* The pattern every Brazilian form has used for twenty years: type eight
|
|
127
|
+
* digits, and logradouro/bairro/cidade/UF arrive on their own. This is the
|
|
128
|
+
* reusable half — masking, validation, debouncing, race-safety and status —
|
|
129
|
+
* with the actual lookup injected via `onLookup`, so the same component serves
|
|
130
|
+
* a supplier form, a store-location form or a delivery checkout without
|
|
131
|
+
* knowing anything about providers or auth.
|
|
132
|
+
*
|
|
133
|
+
* Resolution is best-effort by design. A provider outage, an unknown CEP or a
|
|
134
|
+
* timeout leaves the field exactly as usable as a plain text input — the user
|
|
135
|
+
* types the address themselves. Autofill never blocks.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```tsx
|
|
139
|
+
* <CepField
|
|
140
|
+
* value={cep}
|
|
141
|
+
* onChange={setCep}
|
|
142
|
+
* onLookup={(c) => fetch(`/api/…/lookup/cep/${c}`).then((r) => r.json()).then((b) => b.data)}
|
|
143
|
+
* onResolved={(a) => { setStreet(a.street ?? ''); setCity(a.city ?? ''); }}
|
|
144
|
+
* />
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function CepField({
|
|
148
|
+
value,
|
|
149
|
+
onChange,
|
|
150
|
+
onLookup,
|
|
151
|
+
onResolved,
|
|
152
|
+
onNotFound,
|
|
153
|
+
label = 'CEP',
|
|
154
|
+
placeholder = '00000-000',
|
|
155
|
+
error,
|
|
156
|
+
disabled = false,
|
|
157
|
+
debounceMs = 500,
|
|
158
|
+
name = 'postalCode',
|
|
159
|
+
dataTestId,
|
|
160
|
+
}: CepFieldProps): React.ReactElement {
|
|
161
|
+
const testId = dataTestId ?? `cep-field-${name}`;
|
|
162
|
+
|
|
163
|
+
const status = useCepLookup({
|
|
164
|
+
digits: cepDigits(value),
|
|
165
|
+
complete: isValidCep(value),
|
|
166
|
+
disabled,
|
|
167
|
+
debounceMs,
|
|
168
|
+
onLookup,
|
|
169
|
+
onResolved,
|
|
170
|
+
onNotFound,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const handleChange = useCallback(
|
|
174
|
+
(event: React.ChangeEvent<HTMLInputElement>) => onChange(formatCep(event.target.value)),
|
|
175
|
+
[onChange],
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<FormControl fullWidth>
|
|
180
|
+
{label && (
|
|
181
|
+
<FormLabel htmlFor={name} error={Boolean(error)}>
|
|
182
|
+
{label}
|
|
183
|
+
</FormLabel>
|
|
184
|
+
)}
|
|
185
|
+
<Input
|
|
186
|
+
id={name}
|
|
187
|
+
name={name}
|
|
188
|
+
type="text"
|
|
189
|
+
inputMode="numeric"
|
|
190
|
+
autoComplete="postal-code"
|
|
191
|
+
placeholder={placeholder}
|
|
192
|
+
value={value}
|
|
193
|
+
error={Boolean(error)}
|
|
194
|
+
disabled={disabled}
|
|
195
|
+
loading={status === 'loading'}
|
|
196
|
+
onChange={handleChange}
|
|
197
|
+
data-testid={testId}
|
|
198
|
+
aria-describedby={`${testId}-status`}
|
|
199
|
+
aria-busy={status === 'loading'}
|
|
200
|
+
/>
|
|
201
|
+
{error && (
|
|
202
|
+
<FormMessage error dataTestId={`${testId}-message`}>
|
|
203
|
+
{error}
|
|
204
|
+
</FormMessage>
|
|
205
|
+
)}
|
|
206
|
+
<CepStatus status={status} testId={testId} suppressed={Boolean(error)} />
|
|
207
|
+
</FormControl>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default CepField;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for {@link CepField} — the reusable CEP auto-completer.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A resolved CEP.
|
|
7
|
+
*
|
|
8
|
+
* Structurally identical to what the app's `/lookup/cep` endpoint returns, but
|
|
9
|
+
* declared here so the component stays independent of any particular backend:
|
|
10
|
+
* anything that can produce this shape can drive the field.
|
|
11
|
+
*
|
|
12
|
+
* `número` and `complemento` are deliberately absent — no CEP database holds
|
|
13
|
+
* them, so they are always the user's to type.
|
|
14
|
+
*/
|
|
15
|
+
export interface CepAddress {
|
|
16
|
+
/** The CEP that resolved, bare digits. */
|
|
17
|
+
cep: string;
|
|
18
|
+
/** Logradouro (street name, no number). */
|
|
19
|
+
street: string | null;
|
|
20
|
+
/** Bairro. */
|
|
21
|
+
neighborhood: string | null;
|
|
22
|
+
/** Município. */
|
|
23
|
+
city: string | null;
|
|
24
|
+
/** UF. */
|
|
25
|
+
state: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What the field is currently doing, surfaced as a status line. */
|
|
29
|
+
export type CepLookupStatus = 'idle' | 'loading' | 'found' | 'notfound';
|
|
30
|
+
|
|
31
|
+
export interface CepFieldProps {
|
|
32
|
+
/** Current field value. Masked (`01310-100`) or bare — the field re-masks. */
|
|
33
|
+
value: string;
|
|
34
|
+
/**
|
|
35
|
+
* Called on every keystroke with the MASKED value, so the caller's state and
|
|
36
|
+
* what the user sees never diverge.
|
|
37
|
+
*/
|
|
38
|
+
onChange: (masked: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a complete CEP to an address, or `null` when it is unknown.
|
|
41
|
+
*
|
|
42
|
+
* Injected rather than hard-wired: the component knows nothing about
|
|
43
|
+
* providers, transport or auth. Omit it to get a masked, validated CEP input
|
|
44
|
+
* with no lookup at all.
|
|
45
|
+
*/
|
|
46
|
+
onLookup?: (cep: string) => Promise<CepAddress | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Called once a CEP resolves — this is where the caller fills the address
|
|
49
|
+
* fields. Never called for an incomplete, unchanged or unknown CEP.
|
|
50
|
+
*/
|
|
51
|
+
onResolved?: (address: CepAddress) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Called when a COMPLETE CEP fails to resolve (unknown, or every provider
|
|
54
|
+
* down). The counterpart to `onResolved`: a caller that auto-filled from a
|
|
55
|
+
* previous CEP should undo that here, so the form never shows one CEP's
|
|
56
|
+
* street under a different CEP.
|
|
57
|
+
*/
|
|
58
|
+
onNotFound?: () => void;
|
|
59
|
+
/** Visible label. Defaults to `CEP`. */
|
|
60
|
+
label?: string;
|
|
61
|
+
/** Placeholder. Defaults to `00000-000`. */
|
|
62
|
+
placeholder?: string;
|
|
63
|
+
/** Validation error from the caller's form, shown below the input. */
|
|
64
|
+
error?: string;
|
|
65
|
+
/** Disables the input and suppresses lookups. */
|
|
66
|
+
disabled?: boolean;
|
|
67
|
+
/** Milliseconds to wait after the last keystroke before looking up. */
|
|
68
|
+
debounceMs?: number;
|
|
69
|
+
/** Field name / input id. Defaults to `postalCode`. */
|
|
70
|
+
name?: string;
|
|
71
|
+
/** Test id for the input. Defaults to `cep-field-<name>`. */
|
|
72
|
+
dataTestId?: string;
|
|
73
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CheckboxProps } from './Checkbox.types';
|
|
2
|
+
|
|
3
|
+
type CheckboxDefaultedKeys = 'variant' | 'ripple' | 'glow' | 'pulse';
|
|
4
|
+
|
|
5
|
+
type ResolvedCheckboxProps = CheckboxProps &
|
|
6
|
+
Required<Pick<CheckboxProps, CheckboxDefaultedKeys>>;
|
|
7
|
+
|
|
8
|
+
const CHECKBOX_DEFAULTS: Pick<CheckboxProps, CheckboxDefaultedKeys> = {
|
|
9
|
+
variant: 'default',
|
|
10
|
+
ripple: true,
|
|
11
|
+
glow: false,
|
|
12
|
+
pulse: false,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Strips explicitly-undefined props before the merge, so `ripple={undefined}` still
|
|
16
|
+
// falls back to the default exactly as a destructuring default would.
|
|
17
|
+
const definedProps = (props: CheckboxProps): Partial<CheckboxProps> =>
|
|
18
|
+
Object.fromEntries(
|
|
19
|
+
Object.entries(props).filter(([, value]) => value !== undefined),
|
|
20
|
+
) as Partial<CheckboxProps>;
|
|
21
|
+
|
|
22
|
+
export const resolveCheckboxProps = (props: CheckboxProps): ResolvedCheckboxProps =>
|
|
23
|
+
({ ...CHECKBOX_DEFAULTS, ...definedProps(props) }) as ResolvedCheckboxProps;
|
|
24
|
+
|
|
25
|
+
export const makeTestId =
|
|
26
|
+
(dataTestId?: string) =>
|
|
27
|
+
(suffix: string): string =>
|
|
28
|
+
dataTestId ? `${dataTestId}-${suffix}` : `checkbox-${suffix}`;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
styled} from '@mui/material';
|
|
9
9
|
import React from 'react';
|
|
10
10
|
|
|
11
|
+
import { makeTestId, resolveCheckboxProps } from './Checkbox.helpers';
|
|
11
12
|
import type { CheckboxProps } from './Checkbox.types';
|
|
12
13
|
|
|
13
14
|
const pulse = keyframes`
|
|
@@ -99,21 +100,23 @@ const StyledFormHelperText = styled(FormHelperText)(({ theme }) => ({
|
|
|
99
100
|
}));
|
|
100
101
|
|
|
101
102
|
export const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
102
|
-
({
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
(rawProps, ref) => {
|
|
104
|
+
const {
|
|
105
|
+
variant,
|
|
106
|
+
label,
|
|
107
|
+
error,
|
|
108
|
+
helperText,
|
|
109
|
+
loading,
|
|
110
|
+
ripple,
|
|
111
|
+
glow,
|
|
112
|
+
pulse,
|
|
113
|
+
disabled,
|
|
114
|
+
'data-testid': dataTestId,
|
|
115
|
+
...props
|
|
116
|
+
} = resolveCheckboxProps(rawProps);
|
|
115
117
|
const isDisabled = disabled || loading;
|
|
116
|
-
|
|
118
|
+
const testId = makeTestId(dataTestId);
|
|
119
|
+
|
|
117
120
|
const checkbox = (
|
|
118
121
|
<div style={{ position: 'relative', display: 'inline-flex' }}>
|
|
119
122
|
<StyledCheckbox
|
|
@@ -144,7 +147,7 @@ export const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
|
144
147
|
|
|
145
148
|
if (label) {
|
|
146
149
|
return (
|
|
147
|
-
<div data-testid={
|
|
150
|
+
<div data-testid={testId('container')}>
|
|
148
151
|
<StyledFormControlLabel
|
|
149
152
|
control={checkbox}
|
|
150
153
|
label={label}
|
|
@@ -154,7 +157,7 @@ export const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
|
154
157
|
{helperText && (
|
|
155
158
|
<StyledFormHelperText
|
|
156
159
|
error={error}
|
|
157
|
-
data-testid={
|
|
160
|
+
data-testid={testId('helper')}
|
|
158
161
|
>
|
|
159
162
|
{helperText}
|
|
160
163
|
</StyledFormHelperText>
|