@svgrid/grid 1.2.34 → 2.0.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/README.md +6 -5
- package/dist/SvAccordion.svelte +109 -0
- package/dist/SvAccordion.svelte.d.ts +28 -0
- package/dist/SvAlert.svelte +92 -0
- package/dist/SvAlert.svelte.d.ts +27 -0
- package/dist/SvAutoComplete.svelte +47 -18
- package/dist/SvAutoComplete.svelte.d.ts +2 -5
- package/dist/SvAvatar.svelte +75 -0
- package/dist/SvAvatar.svelte.d.ts +14 -0
- package/dist/SvAvatarGroup.svelte +48 -0
- package/dist/SvAvatarGroup.svelte.d.ts +16 -0
- package/dist/SvBadge.svelte +54 -0
- package/dist/SvBadge.svelte.d.ts +23 -0
- package/dist/SvBreadcrumb.svelte +88 -0
- package/dist/SvBreadcrumb.svelte.d.ts +23 -0
- package/dist/SvButtonGroup.svelte +121 -0
- package/dist/SvButtonGroup.svelte.d.ts +16 -0
- package/dist/SvCalendar.svelte +72 -9
- package/dist/SvCalendar.svelte.d.ts +34 -1
- package/dist/SvCard.svelte +71 -0
- package/dist/SvCard.svelte.d.ts +28 -0
- package/dist/SvCarousel.svelte +134 -0
- package/dist/SvCarousel.svelte.d.ts +28 -0
- package/dist/SvCheckBox.svelte +36 -21
- package/dist/SvCheckBox.svelte.d.ts +2 -5
- package/dist/SvChip.svelte +88 -0
- package/dist/SvChip.svelte.d.ts +31 -0
- package/dist/SvCircularProgress.svelte +109 -0
- package/dist/SvCircularProgress.svelte.d.ts +31 -0
- package/dist/SvColorInput.svelte +54 -27
- package/dist/SvColorInput.svelte.d.ts +10 -6
- package/dist/SvComboBox.svelte +119 -33
- package/dist/SvComboBox.svelte.d.ts +18 -6
- package/dist/SvCommand.svelte +197 -0
- package/dist/SvCommand.svelte.d.ts +26 -0
- package/dist/SvContextMenu.svelte +96 -0
- package/dist/SvContextMenu.svelte.d.ts +25 -0
- package/dist/SvCountryInput.svelte +56 -21
- package/dist/SvCountryInput.svelte.d.ts +9 -5
- package/dist/SvDateRangeInput.svelte +256 -0
- package/dist/SvDateRangeInput.svelte.d.ts +37 -0
- package/dist/SvDateTimePicker.svelte +57 -20
- package/dist/SvDateTimePicker.svelte.d.ts +12 -4
- package/dist/SvDivider.svelte +61 -0
- package/dist/SvDivider.svelte.d.ts +22 -0
- package/dist/SvDrawer.svelte +226 -0
- package/dist/SvDrawer.svelte.d.ts +43 -0
- package/dist/SvDropDownList.svelte +114 -35
- package/dist/SvDropDownList.svelte.d.ts +7 -6
- package/dist/SvDurationInput.svelte +108 -0
- package/dist/SvDurationInput.svelte.d.ts +15 -0
- package/dist/SvEmptyState.svelte +59 -0
- package/dist/SvEmptyState.svelte.d.ts +25 -0
- package/dist/SvField.svelte +85 -0
- package/dist/SvField.svelte.d.ts +30 -0
- package/dist/SvFileUpload.svelte +169 -0
- package/dist/SvFileUpload.svelte.d.ts +24 -0
- package/dist/SvForm.svelte +7 -1
- package/dist/SvForm.svelte.d.ts +3 -0
- package/dist/SvGauge.svelte +7 -1
- package/dist/SvGauge.svelte.d.ts +11 -0
- package/dist/SvGrid.controller.svelte.d.ts +6 -0
- package/dist/SvGrid.controller.svelte.js +64 -9
- package/dist/SvGrid.css +33 -0
- package/dist/SvGrid.svelte +116 -3
- package/dist/SvGrid.types.d.ts +366 -0
- package/dist/SvGridBoard.svelte +2281 -0
- package/dist/SvGridBoard.svelte.d.ts +31 -0
- package/dist/SvGridDropdown.svelte +2 -1
- package/dist/SvGridSelect.svelte +255 -0
- package/dist/SvGridSelect.svelte.d.ts +32 -0
- package/dist/SvGroupCell.svelte +109 -0
- package/dist/SvGroupCell.svelte.d.ts +41 -0
- package/dist/SvListBox.svelte +123 -36
- package/dist/SvListBox.svelte.d.ts +12 -8
- package/dist/SvMaskedInput.svelte +53 -13
- package/dist/SvMaskedInput.svelte.d.ts +16 -6
- package/dist/SvMenu.svelte +101 -0
- package/dist/SvMenu.svelte.d.ts +26 -0
- package/dist/SvMenuList.svelte +143 -0
- package/dist/SvMenuList.svelte.d.ts +24 -0
- package/dist/SvModal.svelte +220 -0
- package/dist/SvModal.svelte.d.ts +31 -0
- package/dist/SvMultiSelect.svelte +277 -0
- package/dist/SvMultiSelect.svelte.d.ts +31 -0
- package/dist/SvNavPane.svelte +314 -0
- package/dist/SvNavPane.svelte.d.ts +59 -0
- package/dist/SvNumberInput.svelte +23 -9
- package/dist/SvNumberInput.svelte.d.ts +2 -0
- package/dist/SvOtpInput.svelte +157 -0
- package/dist/SvOtpInput.svelte.d.ts +16 -0
- package/dist/SvPagination.svelte +100 -0
- package/dist/SvPagination.svelte.d.ts +16 -0
- package/dist/SvPasswordInput.svelte +42 -12
- package/dist/SvPasswordInput.svelte.d.ts +13 -6
- package/dist/SvPhoneInput.svelte +43 -11
- package/dist/SvPhoneInput.svelte.d.ts +10 -11
- package/dist/SvPopover.svelte +127 -0
- package/dist/SvPopover.svelte.d.ts +35 -0
- package/dist/SvProgress.svelte +116 -0
- package/dist/SvProgress.svelte.d.ts +32 -0
- package/dist/SvRadioGroup.svelte +40 -22
- package/dist/SvRadioGroup.svelte.d.ts +2 -10
- package/dist/SvRating.svelte +51 -30
- package/dist/SvRating.svelte.d.ts +10 -7
- package/dist/SvRichText.svelte +192 -0
- package/dist/SvRichText.svelte.d.ts +16 -0
- package/dist/SvRowGroupPanel.svelte +147 -0
- package/dist/SvRowGroupPanel.svelte.d.ts +17 -0
- package/dist/SvScrollArea.svelte +50 -0
- package/dist/SvScrollArea.svelte.d.ts +22 -0
- package/dist/SvSkeleton.svelte +76 -0
- package/dist/SvSkeleton.svelte.d.ts +25 -0
- package/dist/SvSlider.svelte +41 -9
- package/dist/SvSlider.svelte.d.ts +4 -4
- package/dist/SvSparkline.svelte +77 -0
- package/dist/SvSparkline.svelte.d.ts +28 -0
- package/dist/SvSplitter.svelte +138 -0
- package/dist/SvSplitter.svelte.d.ts +32 -0
- package/dist/SvStat.svelte +86 -0
- package/dist/SvStat.svelte.d.ts +30 -0
- package/dist/SvStepper.svelte +91 -0
- package/dist/SvStepper.svelte.d.ts +24 -0
- package/dist/SvSwitchButton.svelte +39 -24
- package/dist/SvSwitchButton.svelte.d.ts +2 -5
- package/dist/SvTabs.svelte +147 -28
- package/dist/SvTabs.svelte.d.ts +16 -4
- package/dist/SvTagsInput.svelte +59 -22
- package/dist/SvTagsInput.svelte.d.ts +9 -4
- package/dist/SvTextArea.svelte +113 -0
- package/dist/SvTextArea.svelte.d.ts +18 -0
- package/dist/SvTextInput.svelte +115 -0
- package/dist/SvTextInput.svelte.d.ts +20 -0
- package/dist/SvTimePicker.svelte +19 -5
- package/dist/SvTimePicker.svelte.d.ts +12 -0
- package/dist/SvTimeline.svelte +75 -0
- package/dist/SvTimeline.svelte.d.ts +24 -0
- package/dist/SvToaster.svelte +124 -0
- package/dist/SvToaster.svelte.d.ts +10 -0
- package/dist/SvToggleButton.svelte +37 -12
- package/dist/SvToggleButton.svelte.d.ts +2 -4
- package/dist/SvTooltip.svelte +113 -0
- package/dist/SvTooltip.svelte.d.ts +22 -0
- package/dist/SvTour.svelte +151 -0
- package/dist/SvTour.svelte.d.ts +25 -0
- package/dist/SvTree.svelte +286 -44
- package/dist/SvTree.svelte.d.ts +26 -9
- package/dist/SvTreeSelect.svelte +222 -0
- package/dist/SvTreeSelect.svelte.d.ts +24 -0
- package/dist/a11y/dismissable.d.ts +32 -0
- package/dist/a11y/dismissable.js +54 -0
- package/dist/a11y/focus-trap.d.ts +39 -0
- package/dist/a11y/focus-trap.js +142 -0
- package/dist/a11y/live-region.d.ts +8 -0
- package/dist/a11y/live-region.js +60 -0
- package/dist/a11y/scroll-lock.d.ts +4 -0
- package/dist/a11y/scroll-lock.js +41 -0
- package/dist/a11y.d.ts +3 -1
- package/dist/a11y.js +1 -1
- package/dist/avatar.d.ts +10 -0
- package/dist/avatar.js +22 -0
- package/dist/builtin-editors.d.ts +5 -0
- package/dist/builtin-editors.js +55 -0
- package/dist/cdn/svgrid.js +16021 -8234
- package/dist/cdn/svgrid.svelte-external.js +14542 -6764
- package/dist/columns.js +3 -2
- package/dist/core.d.ts +7 -1
- package/dist/countries.d.ts +12 -0
- package/dist/countries.js +24 -0
- package/dist/createAccordion.svelte.d.ts +71 -0
- package/dist/createAccordion.svelte.js +99 -0
- package/dist/createAutocomplete.svelte.d.ts +13 -4
- package/dist/createAutocomplete.svelte.js +11 -1
- package/dist/createButtonGroup.svelte.d.ts +62 -0
- package/dist/createButtonGroup.svelte.js +154 -0
- package/dist/createCalendar.svelte.d.ts +5 -0
- package/dist/createCalendar.svelte.js +2 -0
- package/dist/createCheckbox.svelte.d.ts +9 -17
- package/dist/createCheckbox.svelte.js +28 -1
- package/dist/createColorInput.svelte.d.ts +12 -22
- package/dist/createColorInput.svelte.js +13 -0
- package/dist/createCombobox.svelte.d.ts +16 -5
- package/dist/createCombobox.svelte.js +13 -2
- package/dist/createCountryInput.svelte.d.ts +14 -3
- package/dist/createCountryInput.svelte.js +12 -2
- package/dist/createDateTimePicker.svelte.d.ts +17 -2
- package/dist/createDateTimePicker.svelte.js +12 -2
- package/dist/createDropdownList.svelte.d.ts +14 -5
- package/dist/createDropdownList.svelte.js +50 -1
- package/dist/createFileUpload.svelte.d.ts +68 -0
- package/dist/createFileUpload.svelte.js +122 -0
- package/dist/createListbox.svelte.d.ts +10 -1
- package/dist/createListbox.svelte.js +48 -1
- package/dist/createMaskedInput.svelte.d.ts +11 -2
- package/dist/createMaskedInput.svelte.js +11 -1
- package/dist/createNumberInput.svelte.d.ts +1 -0
- package/dist/createNumberInput.svelte.js +1 -0
- package/dist/createPasswordInput.svelte.d.ts +19 -21
- package/dist/createPasswordInput.svelte.js +16 -4
- package/dist/createPhoneInput.svelte.d.ts +26 -7
- package/dist/createPhoneInput.svelte.js +24 -3
- package/dist/createPopoverSelect.svelte.d.ts +86 -0
- package/dist/createPopoverSelect.svelte.js +196 -0
- package/dist/createRadioGroup.svelte.d.ts +9 -0
- package/dist/createRadioGroup.svelte.js +11 -1
- package/dist/createRating.svelte.d.ts +11 -0
- package/dist/createRating.svelte.js +12 -1
- package/dist/createSlider.svelte.d.ts +2 -0
- package/dist/createSlider.svelte.js +16 -5
- package/dist/createSplitter.svelte.d.ts +67 -0
- package/dist/createSplitter.svelte.js +99 -0
- package/dist/createSwitch.svelte.d.ts +9 -16
- package/dist/createSwitch.svelte.js +28 -1
- package/dist/createTabs.svelte.d.ts +7 -0
- package/dist/createTabs.svelte.js +17 -2
- package/dist/createTagsInput.svelte.d.ts +13 -1
- package/dist/createTagsInput.svelte.js +12 -3
- package/dist/createToggle.svelte.d.ts +9 -0
- package/dist/createToggle.svelte.js +12 -1
- package/dist/createTree.svelte.d.ts +19 -0
- package/dist/createTree.svelte.js +108 -15
- package/dist/debounce.d.ts +11 -0
- package/dist/debounce.js +30 -0
- package/dist/duration.d.ts +15 -0
- package/dist/duration.js +51 -0
- package/dist/editor-contract.d.ts +62 -7
- package/dist/editor-contract.js +45 -5
- package/dist/editor-registry.d.ts +67 -0
- package/dist/editor-registry.js +38 -0
- package/dist/editors/cell-editors.d.ts +1 -1
- package/dist/editors/cell-editors.js +5 -0
- package/dist/fuzzy.d.ts +3 -0
- package/dist/fuzzy.js +41 -0
- package/dist/grid-select.d.ts +9 -0
- package/dist/grid-select.js +8 -0
- package/dist/index.d.ts +79 -6
- package/dist/index.js +78 -4
- package/dist/keyboard-handlers.js +19 -0
- package/dist/list-option.d.ts +36 -0
- package/dist/list-option.js +68 -0
- package/dist/otp.d.ts +16 -0
- package/dist/otp.js +26 -0
- package/dist/paginate.d.ts +20 -0
- package/dist/paginate.js +40 -0
- package/dist/popover.d.ts +15 -1
- package/dist/popover.js +29 -3
- package/dist/recurrence.d.ts +36 -0
- package/dist/recurrence.js +90 -0
- package/dist/server-data-source.d.ts +89 -0
- package/dist/server-data-source.js +4 -0
- package/dist/server-group-model.d.ts +98 -0
- package/dist/server-group-model.js +263 -0
- package/dist/toast-store.svelte.d.ts +41 -0
- package/dist/toast-store.svelte.js +113 -0
- package/dist/tree-select.d.ts +19 -0
- package/dist/tree-select.js +40 -0
- package/dist/ui-app.types.d.ts +80 -0
- package/dist/ui-app.types.js +7 -0
- package/dist/validators.d.ts +43 -0
- package/dist/validators.js +53 -0
- package/dist/virtual.d.ts +35 -0
- package/dist/virtual.js +34 -0
- package/package.json +1 -1
- package/src/SvAccordion.svelte +109 -0
- package/src/SvAccordion.test.ts +66 -0
- package/src/SvAlert.svelte +92 -0
- package/src/SvAutoComplete.svelte +47 -18
- package/src/SvAvatar.svelte +75 -0
- package/src/SvAvatarGroup.svelte +48 -0
- package/src/SvBadge.svelte +54 -0
- package/src/SvBreadcrumb.svelte +88 -0
- package/src/SvButtonGroup.svelte +121 -0
- package/src/SvButtonGroup.test.ts +72 -0
- package/src/SvCalendar.svelte +72 -9
- package/src/SvCard.svelte +71 -0
- package/src/SvCarousel.svelte +134 -0
- package/src/SvCheckBox.svelte +36 -21
- package/src/SvChip.svelte +88 -0
- package/src/SvCircularProgress.svelte +109 -0
- package/src/SvColorInput.svelte +54 -27
- package/src/SvComboBox.svelte +119 -33
- package/src/SvCommand.svelte +197 -0
- package/src/SvContextMenu.svelte +96 -0
- package/src/SvCountryInput.svelte +56 -21
- package/src/SvDateRangeInput.svelte +256 -0
- package/src/SvDateRangeInput.test.ts +80 -0
- package/src/SvDateTimePicker.svelte +57 -20
- package/src/SvDivider.svelte +61 -0
- package/src/SvDrawer.svelte +226 -0
- package/src/SvDrawer.test.ts +125 -0
- package/src/SvDropDownList.svelte +114 -35
- package/src/SvDurationInput.svelte +108 -0
- package/src/SvEmptyState.svelte +59 -0
- package/src/SvField.svelte +85 -0
- package/src/SvFileUpload.svelte +169 -0
- package/src/SvFileUpload.test.ts +59 -0
- package/src/SvForm.svelte +7 -1
- package/src/SvGauge.svelte +7 -1
- package/src/SvGrid.controller.svelte.ts +68 -9
- package/src/SvGrid.css +33 -0
- package/src/SvGrid.svelte +116 -3
- package/src/SvGrid.types.ts +374 -0
- package/src/SvGridBoard.svelte +2281 -0
- package/src/SvGridDropdown.svelte +2 -1
- package/src/SvGridSelect.svelte +255 -0
- package/src/SvGridSelect.test.ts +100 -0
- package/src/SvGroupCell.svelte +109 -0
- package/src/SvListBox.svelte +123 -36
- package/src/SvMaskedInput.svelte +53 -13
- package/src/SvMenu.svelte +101 -0
- package/src/SvMenu.test.ts +73 -0
- package/src/SvMenuList.svelte +143 -0
- package/src/SvModal.svelte +220 -0
- package/src/SvMultiSelect.svelte +277 -0
- package/src/SvMultiSelect.test.ts +167 -0
- package/src/SvNavPane.svelte +314 -0
- package/src/SvNavPane.test.ts +70 -0
- package/src/SvNumberInput.svelte +23 -9
- package/src/SvOtpInput.svelte +157 -0
- package/src/SvPagination.svelte +100 -0
- package/src/SvPasswordInput.svelte +42 -12
- package/src/SvPhoneInput.svelte +43 -11
- package/src/SvPopover.svelte +127 -0
- package/src/SvProgress.svelte +116 -0
- package/src/SvRadioGroup.svelte +40 -22
- package/src/SvRating.svelte +51 -30
- package/src/SvRichText.svelte +192 -0
- package/src/SvRowGroupPanel.svelte +147 -0
- package/src/SvScrollArea.svelte +50 -0
- package/src/SvSkeleton.svelte +76 -0
- package/src/SvSlider.svelte +41 -9
- package/src/SvSparkline.svelte +77 -0
- package/src/SvSplitter.svelte +138 -0
- package/src/SvSplitter.test.ts +55 -0
- package/src/SvStat.svelte +86 -0
- package/src/SvStepper.svelte +91 -0
- package/src/SvSwitchButton.svelte +39 -24
- package/src/SvTabs.svelte +147 -28
- package/src/SvTagsInput.svelte +59 -22
- package/src/SvTextArea.svelte +113 -0
- package/src/SvTextInput.svelte +115 -0
- package/src/SvTimePicker.svelte +19 -5
- package/src/SvTimeline.svelte +75 -0
- package/src/SvToaster.svelte +124 -0
- package/src/SvToaster.test.ts +63 -0
- package/src/SvToggleButton.svelte +37 -12
- package/src/SvTooltip.svelte +113 -0
- package/src/SvTour.svelte +151 -0
- package/src/SvTree.svelte +286 -44
- package/src/SvTreeSelect.svelte +222 -0
- package/src/SvTreeSelect.test.ts +87 -0
- package/src/a11y/dismissable.test.ts +95 -0
- package/src/a11y/dismissable.ts +94 -0
- package/src/a11y/focus-trap.test.ts +110 -0
- package/src/a11y/focus-trap.ts +134 -0
- package/src/a11y/live-region.test.ts +40 -0
- package/src/a11y/live-region.ts +65 -0
- package/src/a11y/scroll-lock.test.ts +38 -0
- package/src/a11y/scroll-lock.ts +41 -0
- package/src/a11y.ts +3 -1
- package/src/avatar.test.ts +32 -0
- package/src/avatar.ts +21 -0
- package/src/board.test.ts +927 -0
- package/src/builtin-editors.grid.test.ts +85 -0
- package/src/builtin-editors.test.ts +58 -0
- package/src/builtin-editors.ts +56 -0
- package/src/columns.ts +3 -2
- package/src/core.ts +10 -0
- package/src/countries.ts +25 -0
- package/src/createAccordion.svelte.ts +126 -0
- package/src/createAutocomplete.svelte.ts +18 -1
- package/src/createButtonGroup.svelte.ts +166 -0
- package/src/createCalendar.svelte.ts +6 -0
- package/src/createCheckbox.svelte.ts +23 -1
- package/src/createColorInput.svelte.ts +20 -0
- package/src/createCombobox.svelte.ts +22 -2
- package/src/createCountryInput.svelte.ts +21 -2
- package/src/createDateTimePicker.svelte.ts +23 -2
- package/src/createDropdownList.svelte.ts +31 -2
- package/src/createFileUpload.svelte.ts +144 -0
- package/src/createListbox.svelte.ts +35 -3
- package/src/createMaskedInput.svelte.ts +18 -1
- package/src/createNumberInput.svelte.ts +2 -0
- package/src/createPasswordInput.svelte.ts +31 -5
- package/src/createPhoneInput.svelte.ts +38 -4
- package/src/createPopoverSelect.svelte.ts +208 -0
- package/src/createRadioGroup.svelte.ts +23 -1
- package/src/createRating.svelte.ts +26 -1
- package/src/createSlider.svelte.ts +14 -3
- package/src/createSplitter.svelte.ts +119 -0
- package/src/createSwitch.svelte.ts +26 -3
- package/src/createTabs.svelte.ts +20 -3
- package/src/createTagsInput.svelte.ts +24 -3
- package/src/createToggle.svelte.ts +25 -2
- package/src/createTree.svelte.ts +104 -14
- package/src/debounce.test.ts +50 -0
- package/src/debounce.ts +41 -0
- package/src/duration.test.ts +41 -0
- package/src/duration.ts +51 -0
- package/src/editor-contract.ts +87 -8
- package/src/editor-registry.grid.test.ts +98 -0
- package/src/editor-registry.test.ts +86 -0
- package/src/editor-registry.ts +106 -0
- package/src/editors/cell-editors.ts +10 -1
- package/src/fuzzy.test.ts +40 -0
- package/src/fuzzy.ts +39 -0
- package/src/grid-select.test.ts +28 -0
- package/src/grid-select.ts +20 -0
- package/src/index.ts +150 -5
- package/src/keyboard-handlers.ts +20 -0
- package/src/list-option.ts +74 -0
- package/src/list-power.test.ts +71 -0
- package/src/otp.test.ts +28 -0
- package/src/otp.ts +28 -0
- package/src/overlays.test.ts +85 -0
- package/src/paginate.test.ts +41 -0
- package/src/paginate.ts +61 -0
- package/src/phone-valid.test.ts +26 -0
- package/src/popover.ts +31 -3
- package/src/progress.test.ts +55 -0
- package/src/recurrence.test.ts +91 -0
- package/src/recurrence.ts +111 -0
- package/src/server-data-source.ts +102 -0
- package/src/server-group-model.test.ts +294 -0
- package/src/server-group-model.ts +370 -0
- package/src/svgrid.responsive.test.ts +72 -0
- package/src/toast-store.svelte.ts +148 -0
- package/src/toast-store.test.ts +86 -0
- package/src/tree-edit.test.ts +49 -0
- package/src/tree-filter.test.ts +56 -0
- package/src/tree-select.test.ts +48 -0
- package/src/tree-select.ts +62 -0
- package/src/ui-app.test.ts +130 -0
- package/src/ui-app.types.ts +88 -0
- package/src/ui-display.test.ts +79 -0
- package/src/ui-inputs.test.ts +1 -1
- package/src/ui-status.test.ts +141 -0
- package/src/ui-tier1.test.ts +142 -0
- package/src/ui-tier2.test.ts +124 -0
- package/src/ui-tier3.test.ts +139 -0
- package/src/validators.test.ts +58 -0
- package/src/validators.ts +105 -0
- package/src/virtual.test.ts +72 -0
- package/src/virtual.ts +67 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive (narrow-container) mode: below the breakpoint the grid un-pins
|
|
3
|
+
* columns, drops `hideBelow` columns, and marks the scroll container.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
6
|
+
import { mount, unmount, flushSync } from 'svelte'
|
|
7
|
+
import SvGrid from './SvGrid.svelte'
|
|
8
|
+
|
|
9
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
10
|
+
|
|
11
|
+
/** Force a narrow measured width so `viewportWidth` reads < breakpoint. */
|
|
12
|
+
function withWidth(px: number, fn: () => void) {
|
|
13
|
+
const desc = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientWidth')
|
|
14
|
+
Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, get: () => px })
|
|
15
|
+
try { fn() } finally {
|
|
16
|
+
if (desc) Object.defineProperty(HTMLElement.prototype, 'clientWidth', desc)
|
|
17
|
+
else delete (HTMLElement.prototype as any).clientWidth
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const data = [{ id: 1, a: 'x', b: 'y', c: 'z' }]
|
|
22
|
+
const columns = [
|
|
23
|
+
{ field: 'a', header: 'A', width: 120 },
|
|
24
|
+
{ field: 'b', header: 'B', width: 120 },
|
|
25
|
+
{ field: 'c', header: 'C', width: 120, hideBelow: 500 },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
function render(props: Record<string, unknown>) {
|
|
29
|
+
const target = document.createElement('div')
|
|
30
|
+
document.body.appendChild(target)
|
|
31
|
+
const app = mount(SvGrid as any, { target, props: props as any })
|
|
32
|
+
flushSync()
|
|
33
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('SvGrid responsive mode', () => {
|
|
37
|
+
it('un-pins columns, hides hideBelow columns and marks the container when narrow', () => {
|
|
38
|
+
withWidth(400, () => {
|
|
39
|
+
const { target, destroy } = render({ data, columns, initialColumnPinning: { left: ['a'] }, responsive: true, containerHeight: 300 })
|
|
40
|
+
try {
|
|
41
|
+
const container = target.querySelector('.sv-grid-container')!
|
|
42
|
+
expect(container.classList.contains('sv-grid-narrow')).toBe(true)
|
|
43
|
+
// pinning is suspended -> no sticky pinned cells
|
|
44
|
+
expect(target.querySelectorAll('[data-pinned]')).toHaveLength(0)
|
|
45
|
+
// column c (hideBelow 500, width 400) is dropped; a + b stay
|
|
46
|
+
expect(target.querySelector('[data-col-id="a"]')).not.toBeNull()
|
|
47
|
+
expect(target.querySelector('[data-col-id="c"]')).toBeNull()
|
|
48
|
+
} finally { destroy() }
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('keeps pinning + all columns on a wide container', () => {
|
|
53
|
+
withWidth(1000, () => {
|
|
54
|
+
const { target, destroy } = render({ data, columns, initialColumnPinning: { left: ['a'] }, responsive: true, containerHeight: 300 })
|
|
55
|
+
try {
|
|
56
|
+
expect(target.querySelector('.sv-grid-container')!.classList.contains('sv-grid-narrow')).toBe(false)
|
|
57
|
+
expect(target.querySelectorAll('[data-pinned]').length).toBeGreaterThan(0) // 'a' still pinned
|
|
58
|
+
expect(target.querySelector('[data-col-id="c"]')).not.toBeNull() // 500 < 1000 -> shown
|
|
59
|
+
} finally { destroy() }
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('does nothing without the responsive prop', () => {
|
|
64
|
+
withWidth(400, () => {
|
|
65
|
+
const { target, destroy } = render({ data, columns, initialColumnPinning: { left: ['a'] }, containerHeight: 300 })
|
|
66
|
+
try {
|
|
67
|
+
expect(target.querySelector('.sv-grid-container')!.classList.contains('sv-grid-narrow')).toBe(false)
|
|
68
|
+
expect(target.querySelectorAll('[data-pinned]').length).toBeGreaterThan(0)
|
|
69
|
+
} finally { destroy() }
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* toast-store - the headless model behind <SvToaster>: a singleton reactive
|
|
3
|
+
* queue of transient notifications with auto-dismiss timers, plus the `toast()`
|
|
4
|
+
* API you call from anywhere. Every toast is also pushed to the shared ARIA
|
|
5
|
+
* live region via `announce()`, so screen-reader users hear it even though the
|
|
6
|
+
* visual toast is off to the side.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { toast } from '@svgrid/grid'
|
|
10
|
+
* toast.success('Saved')
|
|
11
|
+
* toast.error('Could not save', { duration: 0 }) // sticky
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Mount a single <SvToaster /> once near the app root to render the queue.
|
|
15
|
+
*/
|
|
16
|
+
import { announce } from './a11y/live-region'
|
|
17
|
+
|
|
18
|
+
export type ToastVariant = 'info' | 'success' | 'warning' | 'error'
|
|
19
|
+
|
|
20
|
+
export type ToastOptions = {
|
|
21
|
+
variant?: ToastVariant
|
|
22
|
+
/** Auto-dismiss after N ms. `0` = sticky (dismiss manually). Default 4000. */
|
|
23
|
+
duration?: number
|
|
24
|
+
/** Show the dismiss (x) button. Default true. */
|
|
25
|
+
dismissible?: boolean
|
|
26
|
+
/** Optional bold title above the message. */
|
|
27
|
+
title?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type Toast = {
|
|
31
|
+
id: number
|
|
32
|
+
message: string
|
|
33
|
+
variant: ToastVariant
|
|
34
|
+
duration: number
|
|
35
|
+
dismissible: boolean
|
|
36
|
+
title?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type VariantOptions = Omit<ToastOptions, 'variant'>
|
|
40
|
+
|
|
41
|
+
export type ToastFn = {
|
|
42
|
+
(message: string, options?: ToastOptions): number
|
|
43
|
+
info: (message: string, options?: VariantOptions) => number
|
|
44
|
+
success: (message: string, options?: VariantOptions) => number
|
|
45
|
+
warning: (message: string, options?: VariantOptions) => number
|
|
46
|
+
error: (message: string, options?: VariantOptions) => number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createToastStore() {
|
|
50
|
+
const toasts = $state<Toast[]>([])
|
|
51
|
+
const timers = new Map<number, ReturnType<typeof setTimeout>>()
|
|
52
|
+
// Remaining ms + when the current run started, so hovering can pause/resume
|
|
53
|
+
// the auto-dismiss countdown instead of losing it.
|
|
54
|
+
const meta = new Map<number, { remaining: number; startedAt: number }>()
|
|
55
|
+
let seq = 0
|
|
56
|
+
|
|
57
|
+
function schedule(id: number, ms: number) {
|
|
58
|
+
meta.set(id, { remaining: ms, startedAt: Date.now() })
|
|
59
|
+
timers.set(id, setTimeout(() => dismiss(id), ms))
|
|
60
|
+
}
|
|
61
|
+
/** Pause a toast's auto-dismiss (e.g. on hover), banking the time left. */
|
|
62
|
+
function pause(id: number): void {
|
|
63
|
+
const timer = timers.get(id)
|
|
64
|
+
const m = meta.get(id)
|
|
65
|
+
if (!timer || !m) return
|
|
66
|
+
clearTimeout(timer)
|
|
67
|
+
timers.delete(id)
|
|
68
|
+
m.remaining = Math.max(0, m.remaining - (Date.now() - m.startedAt))
|
|
69
|
+
}
|
|
70
|
+
/** Resume a paused toast with the banked time remaining. */
|
|
71
|
+
function resume(id: number): void {
|
|
72
|
+
const m = meta.get(id)
|
|
73
|
+
if (!m || timers.has(id) || m.remaining <= 0) return
|
|
74
|
+
m.startedAt = Date.now()
|
|
75
|
+
timers.set(id, setTimeout(() => dismiss(id), m.remaining))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function dismiss(id: number): void {
|
|
79
|
+
const i = toasts.findIndex((t) => t.id === id)
|
|
80
|
+
if (i >= 0) toasts.splice(i, 1)
|
|
81
|
+
const timer = timers.get(id)
|
|
82
|
+
if (timer) {
|
|
83
|
+
clearTimeout(timer)
|
|
84
|
+
timers.delete(id)
|
|
85
|
+
}
|
|
86
|
+
meta.delete(id)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clear(): void {
|
|
90
|
+
for (const timer of timers.values()) clearTimeout(timer)
|
|
91
|
+
timers.clear()
|
|
92
|
+
meta.clear()
|
|
93
|
+
toasts.splice(0, toasts.length)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function push(message: string, options: ToastOptions = {}): number {
|
|
97
|
+
const id = ++seq
|
|
98
|
+
const variant = options.variant ?? 'info'
|
|
99
|
+
const duration = options.duration ?? 4000
|
|
100
|
+
const entry: Toast = {
|
|
101
|
+
id,
|
|
102
|
+
message,
|
|
103
|
+
variant,
|
|
104
|
+
duration,
|
|
105
|
+
dismissible: options.dismissible ?? true,
|
|
106
|
+
title: options.title,
|
|
107
|
+
}
|
|
108
|
+
toasts.push(entry)
|
|
109
|
+
// Errors/warnings interrupt (assertive); info/success are polite.
|
|
110
|
+
announce(entry.title ? `${entry.title}: ${message}` : message, {
|
|
111
|
+
assertive: variant === 'error' || variant === 'warning',
|
|
112
|
+
})
|
|
113
|
+
if (duration > 0) schedule(id, duration)
|
|
114
|
+
return id
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const toast = push as ToastFn
|
|
118
|
+
toast.info = (m, o = {}) => push(m, { ...o, variant: 'info' })
|
|
119
|
+
toast.success = (m, o = {}) => push(m, { ...o, variant: 'success' })
|
|
120
|
+
toast.warning = (m, o = {}) => push(m, { ...o, variant: 'warning' })
|
|
121
|
+
toast.error = (m, o = {}) => push(m, { ...o, variant: 'error' })
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
get toasts() {
|
|
125
|
+
return toasts
|
|
126
|
+
},
|
|
127
|
+
toast,
|
|
128
|
+
dismiss,
|
|
129
|
+
clear,
|
|
130
|
+
pause,
|
|
131
|
+
resume,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const store = createToastStore()
|
|
136
|
+
|
|
137
|
+
/** Live reactive list of active toasts (read inside a component/effect). */
|
|
138
|
+
export const toastStore = { get toasts() { return store.toasts } }
|
|
139
|
+
/** Show a toast. Returns its id. Has `.info/.success/.warning/.error` helpers. */
|
|
140
|
+
export const toast: ToastFn = store.toast
|
|
141
|
+
/** Dismiss a toast by id. */
|
|
142
|
+
export const dismissToast = (id: number): void => store.dismiss(id)
|
|
143
|
+
/** Remove every toast (and cancel their timers). */
|
|
144
|
+
export const clearToasts = (): void => store.clear()
|
|
145
|
+
/** Pause a toast's auto-dismiss countdown (e.g. while hovered). */
|
|
146
|
+
export const pauseToast = (id: number): void => store.pause(id)
|
|
147
|
+
/** Resume a paused toast's countdown with the time it had left. */
|
|
148
|
+
export const resumeToast = (id: number): void => store.resume(id)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the headless toast store: queueing, variants, auto-dismiss timers,
|
|
3
|
+
* manual dismiss, sticky toasts, and the SR announcement side effect.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { toast, dismissToast, clearToasts, pauseToast, resumeToast, toastStore } from './toast-store.svelte'
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.useFakeTimers()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
clearToasts()
|
|
14
|
+
vi.useRealTimers()
|
|
15
|
+
document.body.innerHTML = ''
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('toast store', () => {
|
|
19
|
+
it('queues a toast with defaults', () => {
|
|
20
|
+
const id = toast('Hello')
|
|
21
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
22
|
+
const t = toastStore.toasts[0]!
|
|
23
|
+
expect(t.id).toBe(id)
|
|
24
|
+
expect(t.message).toBe('Hello')
|
|
25
|
+
expect(t.variant).toBe('info')
|
|
26
|
+
expect(t.dismissible).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('variant helpers set the variant', () => {
|
|
30
|
+
toast.success('ok')
|
|
31
|
+
toast.error('bad')
|
|
32
|
+
toast.warning('hmm')
|
|
33
|
+
expect(toastStore.toasts.map((t) => t.variant)).toEqual(['success', 'error', 'warning'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('auto-dismisses after its duration', () => {
|
|
37
|
+
toast('bye', { duration: 3000 })
|
|
38
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
39
|
+
vi.advanceTimersByTime(2999)
|
|
40
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
41
|
+
vi.advanceTimersByTime(1)
|
|
42
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('duration 0 is sticky (no auto-dismiss)', () => {
|
|
46
|
+
toast('stay', { duration: 0 })
|
|
47
|
+
vi.advanceTimersByTime(60_000)
|
|
48
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('dismissToast removes by id and cancels its timer', () => {
|
|
52
|
+
const id = toast('x', { duration: 5000 })
|
|
53
|
+
dismissToast(id)
|
|
54
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
55
|
+
// Advancing past the original duration must not throw or double-remove.
|
|
56
|
+
vi.advanceTimersByTime(5000)
|
|
57
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('announces via a live region (assertive for errors)', () => {
|
|
61
|
+
toast.error('boom')
|
|
62
|
+
vi.runOnlyPendingTimers()
|
|
63
|
+
// The store routes errors to the assertive live region.
|
|
64
|
+
expect(document.querySelector('[aria-live="assertive"]')).not.toBeNull()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('pause banks the remaining time and resume finishes it (hover to keep)', () => {
|
|
68
|
+
const id = toast('hover me', { duration: 3000 })
|
|
69
|
+
vi.advanceTimersByTime(1000) // 2000 left
|
|
70
|
+
pauseToast(id)
|
|
71
|
+
vi.advanceTimersByTime(10_000) // stays while paused
|
|
72
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
73
|
+
resumeToast(id)
|
|
74
|
+
vi.advanceTimersByTime(1999)
|
|
75
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
76
|
+
vi.advanceTimersByTime(1) // banked 2000ms elapsed
|
|
77
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('clearToasts empties the queue', () => {
|
|
81
|
+
toast('a'); toast('b'); toast('c')
|
|
82
|
+
expect(toastStore.toasts).toHaveLength(3)
|
|
83
|
+
clearToasts()
|
|
84
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the pure tree edit helpers: moveTreeNode (before/after/inside +
|
|
3
|
+
* invalid-move guards) and sortTreeNodes (recursive), plus treeContains.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { moveTreeNode, sortTreeNodes, treeContains, type TreeNode } from './createTree.svelte'
|
|
7
|
+
|
|
8
|
+
const tree: TreeNode[] = [
|
|
9
|
+
{ id: 'a', label: 'A', children: [{ id: 'a1', label: 'A1' }, { id: 'a2', label: 'A2' }] },
|
|
10
|
+
{ id: 'b', label: 'B' },
|
|
11
|
+
{ id: 'c', label: 'C' },
|
|
12
|
+
]
|
|
13
|
+
const ids = (list: TreeNode[]) => list.map((n) => n.id)
|
|
14
|
+
|
|
15
|
+
describe('moveTreeNode', () => {
|
|
16
|
+
it('moves before / after a sibling', () => {
|
|
17
|
+
expect(ids(moveTreeNode(tree, 'c', 'a', 'before'))).toEqual(['c', 'a', 'b'])
|
|
18
|
+
expect(ids(moveTreeNode(tree, 'b', 'c', 'after'))).toEqual(['a', 'c', 'b'])
|
|
19
|
+
})
|
|
20
|
+
it('moves inside a folder (appends to children)', () => {
|
|
21
|
+
const out = moveTreeNode(tree, 'b', 'a', 'inside')
|
|
22
|
+
expect(ids(out)).toEqual(['a', 'c'])
|
|
23
|
+
expect(out[0].children!.map((n) => n.id)).toEqual(['a1', 'a2', 'b'])
|
|
24
|
+
})
|
|
25
|
+
it('refuses to drop a node into its own descendant, or onto itself', () => {
|
|
26
|
+
expect(moveTreeNode(tree, 'a', 'a1', 'inside')).toEqual(tree)
|
|
27
|
+
expect(moveTreeNode(tree, 'a', 'a', 'before')).toEqual(tree)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('sortTreeNodes', () => {
|
|
32
|
+
it('sorts siblings recursively', () => {
|
|
33
|
+
const unsorted: TreeNode[] = [
|
|
34
|
+
{ id: 'z', label: 'Zebra' },
|
|
35
|
+
{ id: 'm', label: 'Mango', children: [{ id: 'p', label: 'Pear' }, { id: 'a', label: 'Apple' }] },
|
|
36
|
+
{ id: 'a', label: 'Ant' },
|
|
37
|
+
]
|
|
38
|
+
const asc = sortTreeNodes(unsorted, (x, y) => x.label.localeCompare(y.label))
|
|
39
|
+
expect(ids(asc)).toEqual(['a', 'm', 'z'])
|
|
40
|
+
expect(asc[1].children!.map((n) => n.label)).toEqual(['Apple', 'Pear'])
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('treeContains', () => {
|
|
45
|
+
it('detects descendants', () => {
|
|
46
|
+
expect(treeContains(tree[0], 'a2')).toBe(true)
|
|
47
|
+
expect(treeContains(tree[0], 'b')).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvTree filter/search: typing a query shows only matching nodes + their
|
|
3
|
+
* ancestors (auto-expanded), and clearing restores the tree.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { mount, unmount, flushSync } from 'svelte'
|
|
7
|
+
import SvTree from './SvTree.svelte'
|
|
8
|
+
|
|
9
|
+
const nodes = [
|
|
10
|
+
{ id: 'src', label: 'src', children: [
|
|
11
|
+
{ id: 'a', label: 'App.svelte' },
|
|
12
|
+
{ id: 'b', label: 'utils.ts' },
|
|
13
|
+
] },
|
|
14
|
+
{ id: 'pub', label: 'public', children: [
|
|
15
|
+
{ id: 'c', label: 'logo.svg' },
|
|
16
|
+
] },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
function mountTree(props: Record<string, unknown>) {
|
|
20
|
+
const target = document.createElement('div')
|
|
21
|
+
document.body.appendChild(target)
|
|
22
|
+
const app = mount(SvTree, { target, props: { nodes, expandedIds: ['src', 'pub'], ...props } as any })
|
|
23
|
+
flushSync()
|
|
24
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
25
|
+
}
|
|
26
|
+
const labels = (t: HTMLElement) => [...t.querySelectorAll('.sv-tree__label')].map((n) => n.textContent)
|
|
27
|
+
|
|
28
|
+
describe('SvTree filter', () => {
|
|
29
|
+
it('shows only matches + ancestors (auto-expanded)', () => {
|
|
30
|
+
const { target, destroy } = mountTree({ filter: 'utils' })
|
|
31
|
+
try {
|
|
32
|
+
const shown = labels(target)
|
|
33
|
+
expect(shown).toContain('src') // ancestor kept
|
|
34
|
+
expect(shown).toContain('utils.ts') // match
|
|
35
|
+
expect(shown).not.toContain('App.svelte')
|
|
36
|
+
expect(shown).not.toContain('public')
|
|
37
|
+
} finally { destroy() }
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('matching an ancestor label shows it (and its subtree stays reachable)', () => {
|
|
41
|
+
const { target, destroy } = mountTree({ filter: 'public' })
|
|
42
|
+
try {
|
|
43
|
+
const shown = labels(target)
|
|
44
|
+
expect(shown).toContain('public')
|
|
45
|
+
expect(shown).not.toContain('src')
|
|
46
|
+
} finally { destroy() }
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('renders a built-in search box when searchable', () => {
|
|
50
|
+
const { target, destroy } = mountTree({ searchable: true })
|
|
51
|
+
try {
|
|
52
|
+
expect(target.querySelector('.sv-tree__search-input')).not.toBeNull()
|
|
53
|
+
expect(labels(target)).toContain('App.svelte') // unfiltered initially
|
|
54
|
+
} finally { destroy() }
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { flattenVisible, findNodePath, branchValues } from './tree-select'
|
|
3
|
+
import type { TreeSelectNode } from './ui-app.types'
|
|
4
|
+
|
|
5
|
+
const tree: TreeSelectNode[] = [
|
|
6
|
+
{ value: 'asia', label: 'Asia', children: [
|
|
7
|
+
{ value: 'jp', label: 'Japan', children: [{ value: 'tokyo', label: 'Tokyo' }] },
|
|
8
|
+
{ value: 'cn', label: 'China' },
|
|
9
|
+
] },
|
|
10
|
+
{ value: 'eu', label: 'Europe', children: [{ value: 'fr', label: 'France' }] },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
describe('flattenVisible', () => {
|
|
14
|
+
it('shows only roots when nothing is expanded', () => {
|
|
15
|
+
const rows = flattenVisible(tree, new Set())
|
|
16
|
+
expect(rows.map((r) => r.node.value)).toEqual(['asia', 'eu'])
|
|
17
|
+
expect(rows.every((r) => r.depth === 0)).toBe(true)
|
|
18
|
+
expect(rows[0]!.hasChildren).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('reveals children of expanded nodes with increasing depth', () => {
|
|
22
|
+
const rows = flattenVisible(tree, new Set(['asia']))
|
|
23
|
+
expect(rows.map((r) => r.node.value)).toEqual(['asia', 'jp', 'cn', 'eu'])
|
|
24
|
+
expect(rows.find((r) => r.node.value === 'jp')!.depth).toBe(1)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('nests deeply when a branch chain is expanded', () => {
|
|
28
|
+
const rows = flattenVisible(tree, new Set(['asia', 'jp']))
|
|
29
|
+
expect(rows.map((r) => r.node.value)).toEqual(['asia', 'jp', 'tokyo', 'cn', 'eu'])
|
|
30
|
+
expect(rows.find((r) => r.node.value === 'tokyo')!.depth).toBe(2)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('findNodePath', () => {
|
|
35
|
+
it('returns the root-to-node path', () => {
|
|
36
|
+
expect(findNodePath(tree, 'tokyo')!.map((n) => n.value)).toEqual(['asia', 'jp', 'tokyo'])
|
|
37
|
+
expect(findNodePath(tree, 'eu')!.map((n) => n.value)).toEqual(['eu'])
|
|
38
|
+
})
|
|
39
|
+
it('returns null for unknown values', () => {
|
|
40
|
+
expect(findNodePath(tree, 'nope')).toBeNull()
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('branchValues', () => {
|
|
45
|
+
it('lists every node that has children', () => {
|
|
46
|
+
expect(branchValues(tree).sort()).toEqual(['asia', 'eu', 'jp'])
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tree-select - pure tree helpers behind <SvTreeSelect>: flatten the visible
|
|
3
|
+
* rows for the current expansion state (so keyboard nav is a flat index), and
|
|
4
|
+
* resolve the path from root to a value (for the trigger label). Framework-free
|
|
5
|
+
* + pure so they are unit-tested directly.
|
|
6
|
+
*/
|
|
7
|
+
import type { TreeSelectNode } from './ui-app.types'
|
|
8
|
+
|
|
9
|
+
export type FlatRow = {
|
|
10
|
+
node: TreeSelectNode
|
|
11
|
+
depth: number
|
|
12
|
+
hasChildren: boolean
|
|
13
|
+
expanded: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The rows currently visible given the `expanded` set, in display order. */
|
|
17
|
+
export function flattenVisible(
|
|
18
|
+
nodes: ReadonlyArray<TreeSelectNode>,
|
|
19
|
+
expanded: ReadonlySet<string | number>,
|
|
20
|
+
depth = 0,
|
|
21
|
+
): FlatRow[] {
|
|
22
|
+
const rows: FlatRow[] = []
|
|
23
|
+
for (const node of nodes) {
|
|
24
|
+
const hasChildren = !!node.children && node.children.length > 0
|
|
25
|
+
const isExpanded = expanded.has(node.value)
|
|
26
|
+
rows.push({ node, depth, hasChildren, expanded: isExpanded })
|
|
27
|
+
if (hasChildren && isExpanded) {
|
|
28
|
+
rows.push(...flattenVisible(node.children!, expanded, depth + 1))
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return rows
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Path of nodes from a root down to `value`, or null if not found. */
|
|
35
|
+
export function findNodePath(
|
|
36
|
+
nodes: ReadonlyArray<TreeSelectNode>,
|
|
37
|
+
value: string | number,
|
|
38
|
+
): TreeSelectNode[] | null {
|
|
39
|
+
for (const node of nodes) {
|
|
40
|
+
if (node.value === value) return [node]
|
|
41
|
+
if (node.children) {
|
|
42
|
+
const sub = findNodePath(node.children, value)
|
|
43
|
+
if (sub) return [node, ...sub]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Every value that has children (useful for "expand all"). */
|
|
50
|
+
export function branchValues(nodes: ReadonlyArray<TreeSelectNode>): Array<string | number> {
|
|
51
|
+
const out: Array<string | number> = []
|
|
52
|
+
const walk = (list: ReadonlyArray<TreeSelectNode>) => {
|
|
53
|
+
for (const n of list) {
|
|
54
|
+
if (n.children && n.children.length) {
|
|
55
|
+
out.push(n.value)
|
|
56
|
+
walk(n.children)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
walk(nodes)
|
|
61
|
+
return out
|
|
62
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component tests for the app-UI set: SvPagination, SvBreadcrumb, SvStepper,
|
|
3
|
+
* SvAvatar, SvContextMenu - semantics, active/current state, and interaction.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { mount, unmount, flushSync } from 'svelte'
|
|
7
|
+
import SvPagination from './SvPagination.svelte'
|
|
8
|
+
import SvBreadcrumb from './SvBreadcrumb.svelte'
|
|
9
|
+
import SvStepper from './SvStepper.svelte'
|
|
10
|
+
import SvAvatar from './SvAvatar.svelte'
|
|
11
|
+
import SvContextMenu from './SvContextMenu.svelte'
|
|
12
|
+
|
|
13
|
+
function mountOn(Comp: any, props: Record<string, unknown>) {
|
|
14
|
+
const target = document.createElement('div')
|
|
15
|
+
document.body.appendChild(target)
|
|
16
|
+
const app = mount(Comp, { target, props: props as any })
|
|
17
|
+
flushSync()
|
|
18
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('SvPagination', () => {
|
|
22
|
+
it('marks the current page with aria-current and disables prev on page 1', () => {
|
|
23
|
+
const { target, destroy } = mountOn(SvPagination, { page: 1, pageCount: 10 })
|
|
24
|
+
try {
|
|
25
|
+
const active = target.querySelector('.sv-pg__btn.is-active')!
|
|
26
|
+
expect(active.getAttribute('aria-current')).toBe('page')
|
|
27
|
+
expect(active.textContent).toBe('1')
|
|
28
|
+
const prev = target.querySelector<HTMLButtonElement>('[aria-label="Previous page"]')!
|
|
29
|
+
expect(prev.disabled).toBe(true)
|
|
30
|
+
} finally { destroy() }
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('emits the page number on click', () => {
|
|
34
|
+
let got: number | undefined
|
|
35
|
+
const { target, destroy } = mountOn(SvPagination, { page: 1, pageCount: 10, onChange: (n: number) => (got = n) })
|
|
36
|
+
try {
|
|
37
|
+
const next = target.querySelector<HTMLButtonElement>('[aria-label="Next page"]')!
|
|
38
|
+
next.click()
|
|
39
|
+
flushSync()
|
|
40
|
+
expect(got).toBe(2)
|
|
41
|
+
} finally { destroy() }
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('SvBreadcrumb', () => {
|
|
46
|
+
const items = [{ label: 'Home', href: '/' }, { label: 'Orders', href: '/orders' }, { label: '#1024' }]
|
|
47
|
+
|
|
48
|
+
it('renders links for non-last crumbs and marks the last as current', () => {
|
|
49
|
+
const { target, destroy } = mountOn(SvBreadcrumb, { items })
|
|
50
|
+
try {
|
|
51
|
+
expect(target.querySelectorAll('a.sv-bc__link')).toHaveLength(2)
|
|
52
|
+
expect(target.querySelector('[aria-current="page"]')!.textContent).toContain('#1024')
|
|
53
|
+
} finally { destroy() }
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('collapses the middle when over maxItems', () => {
|
|
57
|
+
const many = [
|
|
58
|
+
{ label: 'A', href: '/a' }, { label: 'B', href: '/b' }, { label: 'C', href: '/c' },
|
|
59
|
+
{ label: 'D', href: '/d' }, { label: 'E' },
|
|
60
|
+
]
|
|
61
|
+
const { target, destroy } = mountOn(SvBreadcrumb, { items: many, maxItems: 3 })
|
|
62
|
+
try {
|
|
63
|
+
expect(target.querySelector('.sv-bc__ellipsis')).not.toBeNull()
|
|
64
|
+
target.querySelector<HTMLButtonElement>('.sv-bc__ellipsis')!.click()
|
|
65
|
+
flushSync()
|
|
66
|
+
expect(target.querySelector('.sv-bc__ellipsis')).toBeNull() // expanded
|
|
67
|
+
} finally { destroy() }
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('SvStepper', () => {
|
|
72
|
+
const steps = [{ label: 'Cart' }, { label: 'Shipping' }, { label: 'Pay' }]
|
|
73
|
+
|
|
74
|
+
it('flags the active step and completes earlier ones', () => {
|
|
75
|
+
const { target, destroy } = mountOn(SvStepper, { steps, current: 1 })
|
|
76
|
+
try {
|
|
77
|
+
const items = target.querySelectorAll('.sv-step__item')
|
|
78
|
+
expect(items[0]!.classList.contains('is-complete')).toBe(true)
|
|
79
|
+
expect(items[1]!.classList.contains('is-active')).toBe(true)
|
|
80
|
+
expect(items[2]!.classList.contains('is-upcoming')).toBe(true)
|
|
81
|
+
expect(target.querySelector('[aria-current="step"]')).not.toBeNull()
|
|
82
|
+
} finally { destroy() }
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('disables upcoming steps in linear mode', () => {
|
|
86
|
+
const { target, destroy } = mountOn(SvStepper, { steps, current: 1, linear: true })
|
|
87
|
+
try {
|
|
88
|
+
const btns = target.querySelectorAll<HTMLButtonElement>('.sv-step__btn')
|
|
89
|
+
expect(btns[2]!.disabled).toBe(true) // upcoming, not reachable
|
|
90
|
+
expect(btns[0]!.disabled).toBe(false) // completed, reachable
|
|
91
|
+
} finally { destroy() }
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('SvAvatar', () => {
|
|
96
|
+
it('renders initials with an img role when there is no src', () => {
|
|
97
|
+
const { target, destroy } = mountOn(SvAvatar, { name: 'Ada Lovelace' })
|
|
98
|
+
try {
|
|
99
|
+
const el = target.querySelector('.sv-avatar')!
|
|
100
|
+
expect(el.getAttribute('role')).toBe('img')
|
|
101
|
+
expect(el.getAttribute('aria-label')).toBe('Ada Lovelace')
|
|
102
|
+
expect(target.querySelector('.sv-avatar__initials')!.textContent).toBe('AL')
|
|
103
|
+
} finally { destroy() }
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('renders a status dot when status is set', () => {
|
|
107
|
+
const { target, destroy } = mountOn(SvAvatar, { name: 'X', status: 'online' })
|
|
108
|
+
try {
|
|
109
|
+
expect(target.querySelector('.sv-avatar__status.is-online')).not.toBeNull()
|
|
110
|
+
} finally { destroy() }
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('SvContextMenu', () => {
|
|
115
|
+
it('opens a menu at the pointer on contextmenu and closes on Escape', () => {
|
|
116
|
+
const items = [{ label: 'Copy' }, { label: 'Delete' }]
|
|
117
|
+
const { target, destroy } = mountOn(SvContextMenu, { items })
|
|
118
|
+
try {
|
|
119
|
+
const zone = target.querySelector('.sv-ctx__zone')!
|
|
120
|
+
zone.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: 40, clientY: 40 }))
|
|
121
|
+
flushSync()
|
|
122
|
+
expect(document.body.querySelector('.sv-ctx')).not.toBeNull()
|
|
123
|
+
expect(document.body.querySelectorAll('[role="menuitem"]').length).toBe(2)
|
|
124
|
+
|
|
125
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
126
|
+
flushSync()
|
|
127
|
+
expect(document.body.querySelector('.sv-ctx')).toBeNull()
|
|
128
|
+
} finally { destroy() }
|
|
129
|
+
})
|
|
130
|
+
})
|