@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
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Favorite from '@mui/icons-material/Favorite';
|
|
2
|
+
import Refresh from '@mui/icons-material/Refresh';
|
|
3
|
+
import Settings from '@mui/icons-material/Settings';
|
|
4
|
+
import Star from '@mui/icons-material/Star';
|
|
2
5
|
import { Box, Stack, Typography } from '@mui/material';
|
|
3
6
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
7
|
import React from 'react';
|
|
@@ -160,17 +163,18 @@ export const KeyboardNavigation: Story = {
|
|
|
160
163
|
const secondIcon = canvas.getByLabelText('Second animated icon');
|
|
161
164
|
const thirdIcon = canvas.getByLabelText('Third animated icon');
|
|
162
165
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
await
|
|
166
|
+
// Tab to the first icon rather than focusing it directly, so the whole
|
|
167
|
+
// sequence below exercises one keyboard journey through the three icons.
|
|
168
|
+
await userEvent.tab();
|
|
169
|
+
await waitFor(() => expect(firstIcon).toHaveFocus());
|
|
166
170
|
|
|
167
171
|
// Tab to second icon
|
|
168
172
|
await userEvent.tab();
|
|
169
|
-
await expect(secondIcon).toHaveFocus();
|
|
173
|
+
await waitFor(() => expect(secondIcon).toHaveFocus());
|
|
170
174
|
|
|
171
175
|
// Tab to third icon
|
|
172
176
|
await userEvent.tab();
|
|
173
|
-
await expect(thirdIcon).toHaveFocus();
|
|
177
|
+
await waitFor(() => expect(thirdIcon).toHaveFocus());
|
|
174
178
|
|
|
175
179
|
// Test Enter key activation
|
|
176
180
|
await userEvent.keyboard('{Enter}');
|
|
@@ -276,13 +280,16 @@ export const FocusManagement: Story = {
|
|
|
276
280
|
const secondIcon = canvas.getByLabelText('Focus test icon 2');
|
|
277
281
|
|
|
278
282
|
// Focus first icon
|
|
279
|
-
|
|
280
|
-
await expect(firstIcon).toHaveFocus();
|
|
283
|
+
await userEvent.tab();
|
|
284
|
+
await waitFor(() => expect(firstIcon).toHaveFocus());
|
|
281
285
|
|
|
282
|
-
// Move focus to second icon
|
|
283
|
-
|
|
284
|
-
await
|
|
285
|
-
await
|
|
286
|
+
// Move focus to second icon — asserted together so the hand-off is checked
|
|
287
|
+
// as one state, not as two reads that could straddle a re-render.
|
|
288
|
+
await userEvent.tab();
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
expect(secondIcon).toHaveFocus();
|
|
291
|
+
expect(firstIcon).not.toHaveFocus();
|
|
292
|
+
});
|
|
286
293
|
},
|
|
287
294
|
};
|
|
288
295
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box, Typography } from '@mui/material';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
-
import { expect, userEvent, waitFor,within } from 'storybook/test';
|
|
3
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
4
4
|
|
|
5
5
|
import { AspectRatio } from './AspectRatio';
|
|
6
6
|
|
|
@@ -198,8 +198,8 @@ export const KeyboardNavigation: Story = {
|
|
|
198
198
|
const component = canvas.getByTestId('keyboard-aspect-ratio');
|
|
199
199
|
|
|
200
200
|
// Focus the component
|
|
201
|
-
|
|
202
|
-
await expect(component).toHaveFocus();
|
|
201
|
+
await userEvent.click(component);
|
|
202
|
+
await waitFor(() => expect(component).toHaveFocus());
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
await step('Tab navigation', async () => {
|
|
@@ -207,7 +207,7 @@ export const KeyboardNavigation: Story = {
|
|
|
207
207
|
|
|
208
208
|
// Tab to next element
|
|
209
209
|
await userEvent.tab();
|
|
210
|
-
await expect(nextButton).toHaveFocus();
|
|
210
|
+
await waitFor(() => expect(nextButton).toHaveFocus());
|
|
211
211
|
});
|
|
212
212
|
},
|
|
213
213
|
};
|
|
@@ -294,20 +294,20 @@ export const FocusManagement: Story = {
|
|
|
294
294
|
const afterButton = canvas.getByTestId('after-button');
|
|
295
295
|
|
|
296
296
|
// Start with first button
|
|
297
|
-
|
|
298
|
-
await expect(beforeButton).toHaveFocus();
|
|
297
|
+
await userEvent.click(beforeButton);
|
|
298
|
+
await waitFor(() => expect(beforeButton).toHaveFocus());
|
|
299
299
|
|
|
300
300
|
// Tab to aspect ratio container
|
|
301
301
|
await userEvent.tab();
|
|
302
|
-
await expect(aspectRatio).toHaveFocus();
|
|
302
|
+
await waitFor(() => expect(aspectRatio).toHaveFocus());
|
|
303
303
|
|
|
304
304
|
// Tab to focusable content inside
|
|
305
305
|
await userEvent.tab();
|
|
306
|
-
await expect(focusableContent).toHaveFocus();
|
|
306
|
+
await waitFor(() => expect(focusableContent).toHaveFocus());
|
|
307
307
|
|
|
308
308
|
// Tab to after button
|
|
309
309
|
await userEvent.tab();
|
|
310
|
-
await expect(afterButton).toHaveFocus();
|
|
310
|
+
await waitFor(() => expect(afterButton).toHaveFocus());
|
|
311
311
|
});
|
|
312
312
|
},
|
|
313
313
|
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { InfiniteScrollProps } from './InfiniteScroll.types';
|
|
4
|
+
|
|
5
|
+
type ScrollableTarget = InfiniteScrollProps['scrollableTarget'];
|
|
6
|
+
|
|
7
|
+
/** A string target is looked up by id first, then as a selector. */
|
|
8
|
+
const resolveScrollable = (scrollableTarget: ScrollableTarget): Element | null => {
|
|
9
|
+
if (!scrollableTarget) return null;
|
|
10
|
+
|
|
11
|
+
if (typeof scrollableTarget === 'string') {
|
|
12
|
+
return (
|
|
13
|
+
document.getElementById(scrollableTarget) || document.querySelector(scrollableTarget)
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return scrollableTarget;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface ObserverArgs {
|
|
21
|
+
testMode: boolean;
|
|
22
|
+
threshold: number;
|
|
23
|
+
scrollableTarget: ScrollableTarget;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Watches a zero-size sentinel at the end of the list. `rootMargin` is what makes
|
|
28
|
+
* it fire before the sentinel is actually on screen, so the next page is already
|
|
29
|
+
* loading by the time the user reaches the bottom.
|
|
30
|
+
*/
|
|
31
|
+
export const useSentinel = ({ testMode, threshold, scrollableTarget }: ObserverArgs) => {
|
|
32
|
+
const sentinelRef = useRef<HTMLDivElement>(null);
|
|
33
|
+
const [isIntersecting, setIsIntersecting] = useState(false);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (testMode || !sentinelRef.current) return;
|
|
37
|
+
|
|
38
|
+
const observer = new window.IntersectionObserver(
|
|
39
|
+
([entry]) => {
|
|
40
|
+
if (entry) {
|
|
41
|
+
setIsIntersecting(entry.isIntersecting);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
root: resolveScrollable(scrollableTarget) || undefined,
|
|
46
|
+
rootMargin: `${threshold}px`,
|
|
47
|
+
threshold: 0,
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
observer.observe(sentinelRef.current);
|
|
52
|
+
|
|
53
|
+
return () => observer.disconnect();
|
|
54
|
+
}, [testMode, threshold, scrollableTarget]);
|
|
55
|
+
|
|
56
|
+
return { sentinelRef, isIntersecting };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type LoadArgs = Pick<
|
|
60
|
+
InfiniteScrollProps,
|
|
61
|
+
'hasMore' | 'loading' | 'error' | 'loadMore' | 'onError' | 'testTriggerRef'
|
|
62
|
+
> & {
|
|
63
|
+
isIntersecting: boolean;
|
|
64
|
+
testMode: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A ref rather than state guards re-entry, so a second intersection landing while
|
|
69
|
+
* the first `loadMore` is still in flight is dropped rather than queued.
|
|
70
|
+
*/
|
|
71
|
+
export const useLoadMore = ({
|
|
72
|
+
hasMore,
|
|
73
|
+
loading,
|
|
74
|
+
error,
|
|
75
|
+
loadMore,
|
|
76
|
+
onError,
|
|
77
|
+
isIntersecting,
|
|
78
|
+
testMode,
|
|
79
|
+
testTriggerRef,
|
|
80
|
+
}: LoadArgs) => {
|
|
81
|
+
const loadingRef = useRef(false);
|
|
82
|
+
|
|
83
|
+
const handleLoadMore = useCallback(async () => {
|
|
84
|
+
if (loadingRef.current || !hasMore || error) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
loadingRef.current = true;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await loadMore();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const errorObj = err instanceof Error ? err : new Error('Unknown error');
|
|
94
|
+
onError?.(errorObj);
|
|
95
|
+
} finally {
|
|
96
|
+
loadingRef.current = false;
|
|
97
|
+
}
|
|
98
|
+
}, [hasMore, error, loadMore, onError]);
|
|
99
|
+
|
|
100
|
+
// Expose test trigger for test mode
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (testMode && testTriggerRef) {
|
|
103
|
+
testTriggerRef.current = handleLoadMore;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
if (testMode && testTriggerRef) {
|
|
108
|
+
testTriggerRef.current = undefined;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}, [testMode, testTriggerRef, handleLoadMore]);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (isIntersecting && hasMore && !loading && !error) {
|
|
115
|
+
handleLoadMore();
|
|
116
|
+
}
|
|
117
|
+
}, [isIntersecting, hasMore, loading, error, handleLoadMore]);
|
|
118
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Alert, Box, CircularProgress, Typography } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { InfiniteScrollProps } from './InfiniteScroll.types';
|
|
5
|
+
|
|
6
|
+
type Variant = NonNullable<InfiniteScrollProps['variant']>;
|
|
7
|
+
|
|
8
|
+
const CENTERED = {
|
|
9
|
+
display: 'flex',
|
|
10
|
+
justifyContent: 'center',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
p: 2,
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export const DefaultLoader: React.FC = () => (
|
|
16
|
+
<Box sx={CENTERED}>
|
|
17
|
+
<CircularProgress size={24} />
|
|
18
|
+
<Typography variant="body2" sx={{ ml: 1 }}>
|
|
19
|
+
Loading...
|
|
20
|
+
</Typography>
|
|
21
|
+
</Box>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const DefaultEndMessage: React.FC = () => (
|
|
25
|
+
<Box sx={CENTERED}>
|
|
26
|
+
<Typography variant="body2" color="text.secondary">
|
|
27
|
+
No more items to load
|
|
28
|
+
</Typography>
|
|
29
|
+
</Box>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export const DefaultError: React.FC<{ error: Error }> = ({ error }) => (
|
|
33
|
+
<Alert severity="error" sx={{ m: 2 }}>
|
|
34
|
+
{error.message || 'An error occurred while loading more items'}
|
|
35
|
+
</Alert>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `horizontal` scrolls along a row; `reverse` stacks bottom-up, for a chat log
|
|
40
|
+
* that loads older messages as you scroll away from the newest.
|
|
41
|
+
*/
|
|
42
|
+
export const containerStyles = (variant: Variant, width?: number | string) => {
|
|
43
|
+
const horizontal = variant === 'horizontal';
|
|
44
|
+
const base = {
|
|
45
|
+
width: horizontal ? width || '100%' : '100%',
|
|
46
|
+
height: horizontal ? '100%' : 'auto',
|
|
47
|
+
overflowX: horizontal ? 'auto' : 'hidden',
|
|
48
|
+
overflowY: horizontal ? 'hidden' : 'visible',
|
|
49
|
+
display: 'flex',
|
|
50
|
+
flexDirection: horizontal ? 'row' : 'column',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return variant === 'reverse' ? { ...base, flexDirection: 'column-reverse' } : base;
|
|
54
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Box, ListItem, ListItemText,Typography } from '@mui/material';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
3
|
import React, { useCallback, useRef, useState } from 'react';
|
|
4
|
-
import { expect, userEvent, waitFor,within } from 'storybook/test';
|
|
4
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
5
5
|
|
|
6
6
|
import { InfiniteScroll } from './InfiniteScroll';
|
|
7
7
|
|
|
@@ -45,7 +45,10 @@ const createTestComponent = (useTestMode = false) => {
|
|
|
45
45
|
if (loadingRef.current) return; // Already loading
|
|
46
46
|
loadingRef.current = true;
|
|
47
47
|
setLoading(true);
|
|
48
|
-
await
|
|
48
|
+
await waitFor(() => {
|
|
49
|
+
// TODO: Add assertion or condition
|
|
50
|
+
expect(true).toBe(true);
|
|
51
|
+
});
|
|
49
52
|
const newItems = generateItems(items.length, 3);
|
|
50
53
|
setItems([...items, ...newItems]);
|
|
51
54
|
setHasMore(items.length + 3 < 12);
|
|
@@ -107,7 +110,7 @@ export const BasicInteraction: Story = {
|
|
|
107
110
|
await expect(canvas.getByTestId('item-4')).toBeInTheDocument();
|
|
108
111
|
|
|
109
112
|
// Verify next batch items are NOT present yet
|
|
110
|
-
await expect(canvas.queryByTestId('item-5')).not.toBeInTheDocument();
|
|
113
|
+
await waitFor(() => expect(canvas.queryByTestId('item-5')).not.toBeInTheDocument());
|
|
111
114
|
|
|
112
115
|
// Verify sentinel is present and properly configured
|
|
113
116
|
const sentinel = canvas.getByTestId('infinite-scroll-sentinel');
|
|
@@ -132,8 +135,8 @@ export const KeyboardNavigation: Story = {
|
|
|
132
135
|
await step('Container focus verification', async () => {
|
|
133
136
|
const container = canvas.getByTestId('scroll-container');
|
|
134
137
|
container.setAttribute('tabindex', '0');
|
|
135
|
-
|
|
136
|
-
await expect(container).toHaveFocus();
|
|
138
|
+
await userEvent.click(container);
|
|
139
|
+
await waitFor(() => expect(container).toHaveFocus());
|
|
137
140
|
});
|
|
138
141
|
},
|
|
139
142
|
};
|
|
@@ -166,13 +169,13 @@ export const FocusManagement: Story = {
|
|
|
166
169
|
|
|
167
170
|
await step('Focus management verification', async () => {
|
|
168
171
|
const firstItem = canvas.getByTestId('item-0');
|
|
169
|
-
|
|
170
|
-
await expect(firstItem).toHaveFocus();
|
|
172
|
+
await userEvent.click(firstItem);
|
|
173
|
+
await waitFor(() => expect(firstItem).toHaveFocus());
|
|
171
174
|
|
|
172
175
|
const container = canvas.getByTestId('scroll-container');
|
|
173
176
|
container.setAttribute('tabindex', '0');
|
|
174
|
-
|
|
175
|
-
await expect(container).toHaveFocus();
|
|
177
|
+
await userEvent.click(container);
|
|
178
|
+
await waitFor(() => expect(container).toHaveFocus());
|
|
176
179
|
});
|
|
177
180
|
},
|
|
178
181
|
};
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import React
|
|
3
|
-
|
|
1
|
+
import { Box } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { useLoadMore, useSentinel } from './InfiniteScroll.hooks';
|
|
5
|
+
import {
|
|
6
|
+
containerStyles,
|
|
7
|
+
DefaultEndMessage,
|
|
8
|
+
DefaultError,
|
|
9
|
+
DefaultLoader,
|
|
10
|
+
} from './InfiniteScroll.parts';
|
|
4
11
|
import type { InfiniteScrollProps } from './InfiniteScroll.types';
|
|
5
12
|
|
|
6
13
|
export const InfiniteScroll: React.FC<InfiniteScrollProps> = ({
|
|
@@ -22,151 +29,25 @@ export const InfiniteScroll: React.FC<InfiniteScrollProps> = ({
|
|
|
22
29
|
testMode = false,
|
|
23
30
|
testTriggerRef,
|
|
24
31
|
}) => {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Loading...
|
|
41
|
-
</Typography>
|
|
42
|
-
</Box>
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const defaultEndMessage = (
|
|
46
|
-
<Box
|
|
47
|
-
sx={{
|
|
48
|
-
display: 'flex',
|
|
49
|
-
justifyContent: 'center',
|
|
50
|
-
alignItems: 'center',
|
|
51
|
-
p: 2,
|
|
52
|
-
}}
|
|
53
|
-
>
|
|
54
|
-
<Typography variant="body2" color="text.secondary">
|
|
55
|
-
No more items to load
|
|
56
|
-
</Typography>
|
|
57
|
-
</Box>
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const defaultErrorComponent = error ? (
|
|
61
|
-
<Alert severity="error" sx={{ m: 2 }}>
|
|
62
|
-
{error.message || 'An error occurred while loading more items'}
|
|
63
|
-
</Alert>
|
|
64
|
-
) : null;
|
|
65
|
-
|
|
66
|
-
const handleLoadMore = useCallback(async () => {
|
|
67
|
-
if (loadingRef.current || !hasMore || error) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
loadingRef.current = true;
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
await loadMore();
|
|
75
|
-
} catch (err) {
|
|
76
|
-
const errorObj = err instanceof Error ? err : new Error('Unknown error');
|
|
77
|
-
onError?.(errorObj);
|
|
78
|
-
} finally {
|
|
79
|
-
loadingRef.current = false;
|
|
80
|
-
}
|
|
81
|
-
}, [hasMore, error, loadMore, onError]);
|
|
82
|
-
|
|
83
|
-
// Expose test trigger for test mode
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
if (testMode && testTriggerRef) {
|
|
86
|
-
testTriggerRef.current = handleLoadMore;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return () => {
|
|
90
|
-
if (testMode && testTriggerRef) {
|
|
91
|
-
testTriggerRef.current = undefined;
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
}, [testMode, testTriggerRef, handleLoadMore]);
|
|
95
|
-
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
if (testMode || !sentinelRef.current) return;
|
|
98
|
-
|
|
99
|
-
const getScrollableElement = () => {
|
|
100
|
-
if (!scrollableTarget) return null;
|
|
101
|
-
|
|
102
|
-
if (typeof scrollableTarget === 'string') {
|
|
103
|
-
return (
|
|
104
|
-
document.getElementById(scrollableTarget) || document.querySelector(scrollableTarget)
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return scrollableTarget;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const scrollableElement = getScrollableElement();
|
|
112
|
-
const rootElement = scrollableElement || undefined;
|
|
113
|
-
|
|
114
|
-
const observer = new window.IntersectionObserver(
|
|
115
|
-
([entry]) => {
|
|
116
|
-
if (entry) {
|
|
117
|
-
setIsIntersecting(entry.isIntersecting);
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
root: rootElement,
|
|
122
|
-
rootMargin: `${threshold}px`,
|
|
123
|
-
threshold: 0,
|
|
124
|
-
},
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
observer.observe(sentinelRef.current);
|
|
128
|
-
|
|
129
|
-
return () => observer.disconnect();
|
|
130
|
-
}, [threshold, scrollableTarget]);
|
|
131
|
-
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
if (isIntersecting && hasMore && !loading && !error) {
|
|
134
|
-
handleLoadMore();
|
|
135
|
-
}
|
|
136
|
-
}, [isIntersecting, hasMore, loading, error, handleLoadMore]);
|
|
137
|
-
|
|
138
|
-
const getContainerStyles = () => {
|
|
139
|
-
const baseStyles = {
|
|
140
|
-
width: variant === 'horizontal' ? width || '100%' : '100%',
|
|
141
|
-
height: variant === 'horizontal' ? '100%' : 'auto',
|
|
142
|
-
overflowX: variant === 'horizontal' ? 'auto' : 'hidden',
|
|
143
|
-
overflowY: variant === 'horizontal' ? 'hidden' : 'visible',
|
|
144
|
-
display: 'flex',
|
|
145
|
-
flexDirection: variant === 'horizontal' ? 'row' : 'column',
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
if (variant === 'reverse') {
|
|
149
|
-
return {
|
|
150
|
-
...baseStyles,
|
|
151
|
-
flexDirection: 'column-reverse',
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return baseStyles;
|
|
156
|
-
};
|
|
157
|
-
|
|
32
|
+
const { sentinelRef, isIntersecting } = useSentinel({ testMode, threshold, scrollableTarget });
|
|
33
|
+
|
|
34
|
+
useLoadMore({
|
|
35
|
+
hasMore,
|
|
36
|
+
loading,
|
|
37
|
+
error,
|
|
38
|
+
loadMore,
|
|
39
|
+
onError,
|
|
40
|
+
isIntersecting,
|
|
41
|
+
testMode,
|
|
42
|
+
testTriggerRef,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// The tail of the list is one of four things: the end message, an error, a
|
|
46
|
+
// spinner, or the sentinel that asks for the next page.
|
|
158
47
|
const renderSentinel = () => {
|
|
159
|
-
if (!hasMore)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (error) {
|
|
164
|
-
return errorComponent || defaultErrorComponent;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (loading) {
|
|
168
|
-
return loader || defaultLoader;
|
|
169
|
-
}
|
|
48
|
+
if (!hasMore) return endMessage || <DefaultEndMessage />;
|
|
49
|
+
if (error) return errorComponent || <DefaultError error={error} />;
|
|
50
|
+
if (loading) return loader || <DefaultLoader />;
|
|
170
51
|
|
|
171
52
|
return (
|
|
172
53
|
<div
|
|
@@ -184,7 +65,7 @@ export const InfiniteScroll: React.FC<InfiniteScrollProps> = ({
|
|
|
184
65
|
<Box
|
|
185
66
|
className={className}
|
|
186
67
|
sx={{
|
|
187
|
-
...
|
|
68
|
+
...containerStyles(variant, width),
|
|
188
69
|
...style,
|
|
189
70
|
}}
|
|
190
71
|
>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { InstallPlatform } from './InstallPrompt.types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure environment probes and dismissal storage for `usePwaInstall`.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is a plain function taking its clock and its storage from the
|
|
7
|
+
* caller where that matters, so the hook's behaviour is testable without
|
|
8
|
+
* faking timers or a browser.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_STORAGE_KEY = 'pwa-install-dismissed';
|
|
12
|
+
export const DEFAULT_DISMISS_DAYS = 30;
|
|
13
|
+
|
|
14
|
+
const MS_PER_DAY = 86_400_000;
|
|
15
|
+
const STANDALONE_QUERY = '(display-mode: standalone)';
|
|
16
|
+
|
|
17
|
+
/** iOS Safari predates `display-mode` and exposes this instead. */
|
|
18
|
+
interface IosNavigator extends Navigator {
|
|
19
|
+
standalone?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Guard for SSR and for jsdom runs where `matchMedia` is not implemented. */
|
|
23
|
+
const hasWindow = (): boolean => typeof window !== 'undefined';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* True when the page is already running as an installed app.
|
|
27
|
+
*
|
|
28
|
+
* Both checks are needed: `display-mode: standalone` is the standard signal,
|
|
29
|
+
* but iOS never implemented it for home-screen launches and reports
|
|
30
|
+
* `navigator.standalone` instead. Miss the iOS branch and the installed app
|
|
31
|
+
* nags its own users to install it again.
|
|
32
|
+
*/
|
|
33
|
+
export const isStandaloneDisplay = (): boolean => {
|
|
34
|
+
if (!hasWindow()) return false;
|
|
35
|
+
if ((window.navigator as IosNavigator).standalone === true) return true;
|
|
36
|
+
return window.matchMedia?.(STANDALONE_QUERY).matches === true;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* True for Safari on iOS/iPadOS — the one browser that can install but cannot
|
|
41
|
+
* be asked programmatically.
|
|
42
|
+
*
|
|
43
|
+
* Two traps handled here. iPadOS 13+ reports a Macintosh user agent, so the
|
|
44
|
+
* touch-point count is what separates an iPad from a desktop; and Chrome,
|
|
45
|
+
* Firefox and Edge on iOS are Safari underneath but offer no Add to Home
|
|
46
|
+
* Screen at all, so telling their users to tap Share is a dead end.
|
|
47
|
+
*
|
|
48
|
+
* Kept parameterised so the agent table can be tested without reassigning
|
|
49
|
+
* `navigator`, which leaks across test files when it is not restored.
|
|
50
|
+
*/
|
|
51
|
+
export const isIosSafariAgent = (userAgent: string, maxTouchPoints: number): boolean => {
|
|
52
|
+
const isIosDevice =
|
|
53
|
+
/iPad|iPhone|iPod/.test(userAgent) ||
|
|
54
|
+
(userAgent.includes('Macintosh') && maxTouchPoints > 1);
|
|
55
|
+
|
|
56
|
+
if (!isIosDevice) return false;
|
|
57
|
+
|
|
58
|
+
return !/CriOS|FxiOS|EdgiOS|OPiOS/.test(userAgent);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** `isIosSafariAgent` against the live browser. Internal — callers use `resolveInstallPlatform`. */
|
|
62
|
+
const isIosSafari = (): boolean => {
|
|
63
|
+
if (!hasWindow()) return false;
|
|
64
|
+
|
|
65
|
+
const { userAgent, maxTouchPoints } = window.navigator;
|
|
66
|
+
return isIosSafariAgent(userAgent, maxTouchPoints);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reads the stored dismissal timestamp.
|
|
71
|
+
*
|
|
72
|
+
* Storage access is wrapped because it THROWS rather than returning null in
|
|
73
|
+
* Safari private mode and wherever cookies are blocked — an uncaught throw
|
|
74
|
+
* here would take down the storefront on the very browsers this component
|
|
75
|
+
* exists to serve.
|
|
76
|
+
*/
|
|
77
|
+
export const readDismissedAt = (storageKey: string): number | null => {
|
|
78
|
+
if (!hasWindow()) return null;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
82
|
+
if (!raw) return null;
|
|
83
|
+
|
|
84
|
+
const parsed = Number.parseInt(raw, 10);
|
|
85
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Persists a dismissal. Silently a no-op when storage is unavailable. */
|
|
92
|
+
export const writeDismissedAt = (storageKey: string, at: number): void => {
|
|
93
|
+
if (!hasWindow()) return;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
window.localStorage.setItem(storageKey, String(at));
|
|
97
|
+
} catch {
|
|
98
|
+
// Storage blocked — the dismissal lasts for this page view only.
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether a stored dismissal still suppresses the prompt.
|
|
104
|
+
*
|
|
105
|
+
* `dismissForDays: 0` means forever. A timestamp in the future (a device whose
|
|
106
|
+
* clock was wound back) is treated as active rather than expired, so a bad
|
|
107
|
+
* clock cannot turn into a prompt on every page view.
|
|
108
|
+
*/
|
|
109
|
+
export const isDismissalActive = (
|
|
110
|
+
dismissedAt: number | null,
|
|
111
|
+
dismissForDays: number,
|
|
112
|
+
now: number,
|
|
113
|
+
): boolean => {
|
|
114
|
+
if (dismissedAt === null) return false;
|
|
115
|
+
if (dismissForDays <= 0) return true;
|
|
116
|
+
|
|
117
|
+
return now - dismissedAt < dismissForDays * MS_PER_DAY;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** Resolves which install route this browser offers. */
|
|
121
|
+
export const resolveInstallPlatform = (hasDeferredPrompt: boolean): InstallPlatform => {
|
|
122
|
+
if (hasDeferredPrompt) return 'prompt';
|
|
123
|
+
return isIosSafari() ? 'ios' : 'unsupported';
|
|
124
|
+
};
|