@kubex/zinc 1.1.185 → 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/.browserslistrc +2 -0
- package/.github/workflows/js_build_and_deploy.yaml +54 -1
- package/.github/workflows/test.yaml +27 -0
- package/dist/chunks/zn.UWJLIXDT.js +1 -0
- package/dist/custom-elements.json +10319 -2666
- package/dist/vscode.html-custom-data.json +1012 -57
- package/dist/web-types.json +2097 -145
- package/dist/zn.d.ts +2294 -416
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +2310 -1637
- package/docs/_includes/default.njk +4 -1
- package/docs/assets/scripts/docs.js +22 -6
- package/docs/assets/styles/docs.css +9 -2
- package/docs/data/report-table-day-compare.json +380 -0
- package/docs/data/report-table.json +314 -0
- package/docs/pages/components/avatar-group.md +57 -0
- package/docs/pages/components/badge.md +67 -0
- package/docs/pages/components/button.md +41 -1
- package/docs/pages/components/chat-message.md +16 -0
- package/docs/pages/components/checkbox.md +1 -1
- package/docs/pages/components/chip.md +25 -13
- package/docs/pages/components/color-picker.md +37 -0
- package/docs/pages/components/copy-button.md +2 -2
- package/docs/pages/components/data-table-filter.md +33 -50
- package/docs/pages/components/data-table-search.md +4 -4
- package/docs/pages/components/data-table.md +27 -3
- package/docs/pages/components/date-range.md +116 -0
- package/docs/pages/components/dialog.md +29 -37
- package/docs/pages/components/diff-row.md +28 -0
- package/docs/pages/components/diff.md +46 -0
- package/docs/pages/components/editor.md +55 -0
- package/docs/pages/components/icon.md +46 -1
- package/docs/pages/components/input.md +30 -11
- package/docs/pages/components/item.md +35 -0
- package/docs/pages/components/key-value.md +25 -0
- package/docs/pages/components/menu.md +74 -7
- package/docs/pages/components/nav-item.md +15 -0
- package/docs/pages/components/nav-list.md +132 -0
- package/docs/pages/components/navbar.md +100 -1
- package/docs/pages/components/page.md +242 -74
- package/docs/pages/components/priority-list.md +43 -5
- package/docs/pages/components/query-builder.md +23 -0
- package/docs/pages/components/radio-group.md +33 -1
- package/docs/pages/components/radio.md +1 -1
- package/docs/pages/components/report-table.md +202 -0
- package/docs/pages/components/stat.md +30 -0
- package/docs/pages/components/tile.md +11 -0
- package/docs/pages/components/tooltip.md +1 -1
- package/docs/pages/components/translation-grid.md +86 -0
- package/docs/pages/components/vertical-stepper.md +108 -64
- package/package.json +2 -1
- package/scss/_global-spacing.scss +2 -2
- package/scss/_root.scss +11 -171
- package/scss/boot.scss +14 -0
- package/scss/mixins/_helper-mixins.scss +24 -2
- package/scss/mixins/_motion-mixins.scss +126 -0
- package/scss/mixins/_spacing-mixins.scss +4 -3
- package/scss/mixins/_tab-indicator-mixins.scss +12 -0
- package/scss/mixins/_typography-mixins.scss +1 -1
- package/scss/mixins/index.scss +14 -0
- package/scss/shared/_base.scss +32 -1
- package/scss/shared/_button-mixin.scss +7 -5
- package/scss/shared/_table.scss +35 -4
- package/scss/shared/nav.scss +4 -0
- package/scss/shared/shimmer.scss +1 -1
- package/scss/table-controls.scss +206 -0
- package/scss/themes/_dark.scss +1 -3
- package/scss/themes/_index.scss +1 -1
- package/scss/themes/_light.scss +46 -6
- package/scss/themes/_modes.scss +11 -11
- package/scss/themes/_openbyte.scss +944 -0
- package/scss/themes/_typography.scss +4 -2
- package/scss/variables/index.scss +0 -9
- package/src/components/alert/alert.component.ts +14 -4
- package/src/components/alert/alert.scss +55 -69
- package/src/components/alert/alert.test.ts +8 -0
- package/src/components/animated-button/animated-button.component.ts +3 -2
- package/src/components/animated-button/animated-button.scss +6 -23
- package/src/components/animated-button/animated-button.test.ts +4 -3
- package/src/components/avatar-group/avatar-group.component.ts +84 -0
- package/src/components/avatar-group/avatar-group.scss +54 -0
- package/src/components/avatar-group/avatar-group.test.ts +59 -0
- package/src/components/avatar-group/index.ts +12 -0
- package/src/components/badge/badge.component.ts +49 -0
- package/src/components/badge/badge.scss +80 -0
- package/src/components/badge/badge.test.ts +34 -0
- package/src/components/badge/index.ts +12 -0
- package/src/components/bulk-actions/bulk-actions.component.ts +14 -2
- package/src/components/button/button.component.ts +228 -8
- package/src/components/button/button.scss +115 -32
- package/src/components/button/button.test.ts +122 -0
- package/src/components/button-menu/button-menu.component.ts +32 -11
- package/src/components/button-menu/button-menu.scss +5 -1
- package/src/components/channel-tile/channel-tile.component.ts +34 -27
- package/src/components/channel-tile/channel-tile.scss +76 -43
- package/src/components/chart/builders.test.ts +46 -0
- package/src/components/chart/builders.ts +27 -3
- package/src/components/chart/chart-live.test.ts +4 -6
- package/src/components/chart/chart.component.ts +125 -5
- package/src/components/chart/chart.scss +121 -0
- package/src/components/chart/chart.test.ts +81 -0
- package/src/components/chat-message/chat-message.component.ts +21 -1
- package/src/components/chat-message/chat-message.scss +13 -2
- package/src/components/chat-message/chat-message.test.ts +9 -0
- package/src/components/checkbox/checkbox.component.ts +61 -27
- package/src/components/checkbox/checkbox.scss +58 -25
- package/src/components/checkbox-group/checkbox-group.component.ts +7 -1
- package/src/components/chip/chip.component.ts +11 -1
- package/src/components/chip/chip.scss +91 -26
- package/src/components/chip/chip.test.ts +17 -0
- package/src/components/collapsible/collapsible.component.ts +17 -3
- package/src/components/collapsible/collapsible.scss +19 -10
- package/src/components/color-picker/color-picker.component.ts +277 -0
- package/src/components/color-picker/color-picker.scss +141 -0
- package/src/components/color-picker/color-picker.test.ts +57 -0
- package/src/components/color-picker/index.ts +12 -0
- package/src/components/cols/cols.component.ts +10 -0
- package/src/components/cols/cols.test.ts +17 -0
- package/src/components/confirm/confirm.component.ts +20 -6
- package/src/components/confirm/confirm.test.ts +52 -0
- package/src/components/content-block/content-block.component.ts +38 -9
- package/src/components/content-block/content-block.scss +11 -5
- package/src/components/content-block/content-block.test.ts +26 -0
- package/src/components/copy-button/copy-button.component.ts +2 -2
- package/src/components/copy-button/copy-button.scss +17 -4
- package/src/components/data-select/data-select.component.ts +3 -1
- package/src/components/data-table/data-table.component.ts +362 -140
- package/src/components/data-table/data-table.scss +136 -60
- package/src/components/data-table/data-table.test.ts +356 -4
- package/src/components/data-table-filter/data-table-filter.component.ts +426 -406
- package/src/components/data-table-filter/data-table-filter.scss +176 -148
- package/src/components/data-table-filter/data-table-filter.test.ts +271 -273
- package/src/components/data-table-search/data-table-search.component.ts +4 -2
- package/src/components/data-table-search/data-table-search.scss +8 -1
- package/src/components/data-table-search/data-table-search.test.ts +29 -0
- package/src/components/date-range/date-range-math.test.ts +78 -0
- package/src/components/date-range/date-range-math.ts +105 -0
- package/src/components/date-range/date-range.component.ts +375 -0
- package/src/components/date-range/date-range.scss +272 -0
- package/src/components/date-range/date-range.test.ts +247 -0
- package/src/components/date-range/index.ts +12 -0
- package/src/components/datepicker/datepicker.component.ts +44 -7
- package/src/components/datepicker/datepicker.scss +9 -0
- package/src/components/datepicker/datepicker.test.ts +2 -2
- package/src/components/dialog/dialog.component.ts +38 -19
- package/src/components/dialog/dialog.scss +65 -36
- package/src/components/dialog/dialog.test.ts +14 -0
- package/src/components/diff/diff.component.ts +55 -0
- package/src/components/diff/diff.scss +46 -0
- package/src/components/diff/diff.test.ts +15 -0
- package/src/components/diff/index.ts +12 -0
- package/src/components/diff-row/diff-row.component.ts +75 -0
- package/src/components/diff-row/diff-row.scss +70 -0
- package/src/components/diff-row/diff-row.test.ts +37 -0
- package/src/components/diff-row/index.ts +12 -0
- package/src/components/dropdown/dropdown.component.ts +12 -10
- package/src/components/dropdown/dropdown.scss +2 -0
- package/src/components/editor/editor.component.ts +159 -11
- package/src/components/editor/editor.scss +250 -1
- package/src/components/editor/editor.test.ts +31 -3
- package/src/components/editor/modules/ai/panel/ai-panel.scss +0 -1
- package/src/components/editor/modules/attachment/attachment.ts +53 -31
- package/src/components/editor/modules/context-menu/context-menu.ts +17 -0
- package/src/components/editor/modules/dialog/dialog.component.ts +23 -1
- package/src/components/editor/modules/dialog/dialog.test.ts +28 -0
- package/src/components/editor/modules/emoji/emoji.ts +4 -5
- package/src/components/editor/modules/merge-field/merge-field.ts +83 -0
- package/src/components/editor/modules/toolbar/toolbar.component.ts +121 -68
- package/src/components/editor/modules/toolbar/toolbar.scss +38 -0
- package/src/components/editor/modules/toolbar/toolbar.ts +25 -1
- package/src/components/empty-state/empty-state.component.ts +6 -1
- package/src/components/empty-state/empty-state.scss +21 -4
- package/src/components/expanding-action/expanding-action.component.ts +22 -34
- package/src/components/expanding-action/expanding-action.scss +3 -45
- package/src/components/expanding-action/expanding-action.test.ts +2 -1
- package/src/components/file/file.component.ts +30 -2
- package/src/components/file/file.scss +6 -0
- package/src/components/flow-builder/flow-builder.component.ts +36 -3
- package/src/components/flow-builder/flow-builder.scss +0 -6
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +8 -0
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +10 -5
- package/src/components/flow-builder/modules/flow-node/flow-node.component.ts +17 -0
- package/src/components/flow-builder/modules/flow-node/flow-node.scss +10 -0
- package/src/components/form-actions/form-actions.component.ts +48 -2
- package/src/components/form-actions/form-actions.scss +1 -1
- package/src/components/form-group/form-group.component.ts +10 -3
- package/src/components/form-group/form-group.scss +37 -4
- package/src/components/form-group/form-group.test.ts +10 -0
- package/src/components/header/header.component.ts +11 -4
- package/src/components/header/header.scss +47 -5
- package/src/components/hover-container/hover-container.component.ts +4 -5
- package/src/components/icon/icon.component.ts +95 -9
- package/src/components/icon/icon.scss +70 -6
- package/src/components/icon/icon.test.ts +55 -0
- package/src/components/icon-picker/icon-picker.component.ts +35 -1
- package/src/components/inline-edit/inline-edit.component.ts +38 -8
- package/src/components/inline-edit/inline-edit.scss +128 -19
- package/src/components/input/input.component.ts +173 -297
- package/src/components/input/input.scss +65 -43
- package/src/components/input/input.test.ts +86 -4
- package/src/components/item/item.component.ts +12 -0
- package/src/components/item/item.scss +102 -3
- package/src/components/item/item.test.ts +23 -0
- package/src/components/key/key.component.ts +2 -2
- package/src/components/key/key.scss +11 -0
- package/src/components/key-value/index.ts +12 -0
- package/src/components/key-value/key-value.component.ts +160 -0
- package/src/components/key-value/key-value.scss +20 -0
- package/src/components/key-value/key-value.test.ts +22 -0
- package/src/components/markdown-editor/markdown-editor.component.ts +8 -1
- package/src/components/menu/menu.component.ts +21 -2
- package/src/components/menu/menu.scss +43 -23
- package/src/components/menu/menu.test.ts +33 -0
- package/src/components/menu-item/menu-item.component.ts +13 -2
- package/src/components/menu-item/menu-item.scss +56 -23
- package/src/components/menu-item/menu-item.test.ts +31 -0
- package/src/components/menu-item/submenu-controller.ts +25 -15
- package/src/components/menu-label/index.ts +12 -0
- package/src/components/menu-label/menu-label.component.ts +30 -0
- package/src/components/menu-label/menu-label.scss +18 -0
- package/src/components/nav-item/index.ts +12 -0
- package/src/components/nav-item/nav-item.component.ts +83 -0
- package/src/components/nav-item/nav-item.scss +128 -0
- package/src/components/nav-list/index.ts +12 -0
- package/src/components/nav-list/nav-list.component.ts +313 -0
- package/src/components/nav-list/nav-list.scss +87 -0
- package/src/components/nav-list/nav-list.test.ts +250 -0
- package/src/components/navbar/navbar.component.ts +235 -171
- package/src/components/navbar/navbar.scss +333 -169
- package/src/components/navbar/navbar.test.ts +63 -143
- package/src/components/page/page.component.ts +114 -29
- package/src/components/page/page.scss +223 -95
- package/src/components/page/page.test.ts +214 -3
- package/src/components/page-builder/page-builder.component.ts +51 -5
- package/src/components/page-builder/page-builder.scss +6 -1
- package/src/components/page-nav/page-nav.component.ts +25 -3
- package/src/components/page-nav/page-nav.scss +10 -2
- package/src/components/pagination/pagination.scss +1 -1
- package/src/components/panel/panel.component.ts +14 -13
- package/src/components/panel/panel.scss +14 -10
- package/src/components/panel/panel.test.ts +42 -0
- package/src/components/popup/popup.component.ts +20 -13
- package/src/components/popup/popup.test.ts +21 -1
- package/src/components/priority-list/priority-list.component.ts +157 -172
- package/src/components/priority-list/priority-list.scss +113 -17
- package/src/components/priority-list/priority-list.test.ts +301 -3
- package/src/components/progress-bar/progress-bar.component.ts +1 -1
- package/src/components/progress-bar/progress-bar.scss +16 -4
- package/src/components/progress-tile/progress-tile.scss +1 -1
- package/src/components/query-builder/query-builder.component.ts +85 -10
- package/src/components/query-builder/query-builder.scss +7 -1
- package/src/components/query-builder/query-builder.test.ts +40 -0
- package/src/components/radio/radio.component.ts +57 -18
- package/src/components/radio/radio.scss +101 -26
- package/src/components/radio-group/radio-group.component.ts +45 -7
- package/src/components/radio-group/radio-group.scss +17 -0
- package/src/components/radio-group/radio-group.test.ts +63 -2
- package/src/components/rating/rating.component.ts +23 -0
- package/src/components/rating/rating.scss +1 -1
- package/src/components/remarkd-editor/remarkd-editor.component.ts +60 -5
- package/src/components/remarkd-editor/remarkd-editor.scss +12 -4
- package/src/components/report-table/index.ts +12 -0
- package/src/components/report-table/report-format.test.ts +86 -0
- package/src/components/report-table/report-format.ts +72 -0
- package/src/components/report-table/report-math.test.ts +74 -0
- package/src/components/report-table/report-math.ts +252 -0
- package/src/components/report-table/report-table.component.ts +681 -0
- package/src/components/report-table/report-table.scss +328 -0
- package/src/components/report-table/report-table.test.ts +702 -0
- package/src/components/reveal/reveal.component.ts +15 -1
- package/src/components/reveal/reveal.scss +5 -0
- package/src/components/schedule-builder/schedule-builder.component.ts +23 -8
- package/src/components/scroll-container/scroll-container.component.ts +19 -1
- package/src/components/scroll-container/scroll-container.scss +6 -1
- package/src/components/select/select.component.ts +52 -36
- package/src/components/select/select.scss +42 -25
- package/src/components/select/select.test.ts +14 -2
- package/src/components/settings-container/settings-container.scss +18 -5
- package/src/components/sidebar/sidebar.component.ts +8 -1
- package/src/components/sidebar/sidebar.scss +6 -1
- package/src/components/skeleton/skeleton.component.ts +3 -3
- package/src/components/skeleton/skeleton.scss +6 -6
- package/src/components/skeleton/skeleton.test.ts +9 -0
- package/src/components/slash-menu/slash-menu.scss +1 -1
- package/src/components/slideout/slideout.component.ts +20 -3
- package/src/components/slideout/slideout.scss +13 -6
- package/src/components/slideout/slideout.test.ts +19 -0
- package/src/components/split-button/split-button.component.ts +1 -1
- package/src/components/split-pane/split-pane.component.ts +71 -8
- package/src/components/split-pane/split-pane.scss +145 -41
- package/src/components/split-pane/split-pane.test.ts +23 -3
- package/src/components/stat/stat.component.ts +209 -14
- package/src/components/stat/stat.scss +305 -5
- package/src/components/stat/stat.test.ts +143 -2
- package/src/components/status-indicator/status-indicator.scss +22 -21
- package/src/components/stepper/stepper.scss +3 -3
- package/src/components/tab/tab.component.ts +1 -0
- package/src/components/table/table.component.ts +12 -1
- package/src/components/table/table.scss +21 -13
- package/src/components/table/table.test.ts +17 -0
- package/src/components/tabs/tabs.component.ts +150 -37
- package/src/components/tabs/tabs.scss +48 -7
- package/src/components/tabs/tabs.test.ts +169 -0
- package/src/components/textarea/textarea.component.ts +25 -3
- package/src/components/textarea/textarea.scss +10 -0
- package/src/components/textarea/textarea.test.ts +8 -4
- package/src/components/theme-editor/theme-editor.scss +0 -6
- package/src/components/tile/tile.component.ts +32 -2
- package/src/components/tile/tile.scss +40 -7
- package/src/components/tile/tile.test.ts +10 -0
- package/src/components/toggle/toggle.component.ts +23 -7
- package/src/components/toggle/toggle.scss +36 -29
- package/src/components/toggle/toggle.test.ts +17 -0
- package/src/components/tooltip/tooltip.component.ts +3 -0
- package/src/components/tooltip/tooltip.scss +11 -6
- package/src/components/tooltip/tooltip.test.ts +12 -0
- package/src/components/translation-field/index.ts +12 -0
- package/src/components/translation-field/translation-field.component.ts +55 -0
- package/src/components/translation-field/translation-field.scss +101 -0
- package/src/components/translation-fields/index.ts +12 -0
- package/src/components/translation-fields/translation-fields.component.ts +80 -0
- package/src/components/translation-fields/translation-fields.scss +60 -0
- package/src/components/translation-grid/index.ts +12 -0
- package/src/components/translation-grid/translation-grid.component.ts +157 -0
- package/src/components/translation-grid/translation-grid.scss +138 -0
- package/src/components/translation-grid/translation-grid.test.ts +28 -0
- package/src/components/vertical-stepper/vertical-stepper.component.ts +35 -9
- package/src/components/vertical-stepper/vertical-stepper.scss +110 -39
- package/src/components/vertical-stepper/vertical-stepper.test.ts +35 -0
- package/src/components/well/well.scss +2 -2
- package/src/events/events.ts +2 -0
- package/src/events/zn-chart-click.ts +13 -0
- package/src/events/zn-report-change.ts +7 -0
- package/src/form-control.scss +36 -3
- package/src/internal/anchored-popover.ts +175 -0
- package/src/internal/animate.ts +19 -5
- package/src/internal/cosmic-border.ts +15 -0
- package/src/internal/form.ts +1 -1
- package/src/internal/hover-intent.ts +15 -0
- package/src/internal/input-modality.ts +15 -0
- package/src/internal/pointer-intent.ts +37 -0
- package/src/internal/reorder.ts +632 -0
- package/src/internal/scroll.ts +51 -3
- package/src/internal/selection-card.ts +1 -1
- package/src/internal/tab-indicator.ts +92 -0
- package/src/internal/table-sort.ts +25 -0
- package/src/internal/table-tabs.ts +108 -0
- package/src/internal/test-drag.ts +35 -0
- package/src/internal/theme-openbyte.test.ts +126 -0
- package/src/internal/theme.ts +4 -4
- package/src/internal/zinc-element.ts +2 -2
- package/src/selection-card.scss +25 -0
- package/src/types/web-test-runner-commands.d.ts +1 -0
- package/src/utilities/color.test.ts +43 -0
- package/src/utilities/color.ts +189 -0
- package/src/zinc.ts +14 -0
- package/.github/workflows/eleventy_build.yml +0 -42
- package/scss/themes/_aura.scss +0 -114
|
@@ -77,12 +77,21 @@ export default class ZnRadioGroup extends ZincElement implements ZincFormControl
|
|
|
77
77
|
/** Squares off the corners of every contained radio in the group, which are rounded by default. */
|
|
78
78
|
@property({type: Boolean, reflect: true}) square = false;
|
|
79
79
|
|
|
80
|
+
/** Centres the text of every contained radio in the group. */
|
|
81
|
+
@property({type: Boolean, reflect: true}) centered = false;
|
|
82
|
+
|
|
80
83
|
/**
|
|
81
84
|
* Lays horizontal contained radios out as a wrapping row instead of an equal-width grid, so each one sizes from
|
|
82
85
|
* `--zn-radio-group-column-width` (set it to `auto` to size from content) and wraps onto a new line when needed.
|
|
83
86
|
*/
|
|
84
87
|
@property({type: Boolean, reflect: true}) wrap = false;
|
|
85
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Lays the radios out as a segmented scale, in document order, filling every segment up to the checked one. Suits
|
|
91
|
+
* ordered answers such as 1 to 10, and unlike a range input it has no value until one is picked.
|
|
92
|
+
*/
|
|
93
|
+
@property({type: Boolean, reflect: true}) scale = false;
|
|
94
|
+
|
|
86
95
|
/**
|
|
87
96
|
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
|
|
88
97
|
* to place the form control outside a form and associate it with the form that has this `id`. The form must be in
|
|
@@ -151,14 +160,23 @@ export default class ZnRadioGroup extends ZincElement implements ZincFormControl
|
|
|
151
160
|
}
|
|
152
161
|
|
|
153
162
|
private handleKeyDown(event: KeyboardEvent) {
|
|
154
|
-
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(event.key)) {
|
|
163
|
+
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' ', 'Enter'].includes(event.key)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Keys typed into controls inside a radio's selected content belong to those controls
|
|
168
|
+
if (event.composedPath().some(el => el instanceof HTMLElement && el.slot === 'selected-content')) {
|
|
155
169
|
return;
|
|
156
170
|
}
|
|
157
171
|
|
|
158
172
|
const radios = this.getAllRadios().filter(radio => !radio.disabled);
|
|
159
173
|
const checkedRadio = radios.find(radio => radio.checked) ?? radios[0];
|
|
160
|
-
const
|
|
161
|
-
|
|
174
|
+
const isSelectKey = event.key === ' ' || event.key === 'Enter';
|
|
175
|
+
// Each radio is its own tab stop, so Space/Enter select the focused radio rather than the checked one.
|
|
176
|
+
const focusedRadio = (event.target as HTMLElement).closest<ZnRadio>('zn-radio');
|
|
177
|
+
const fromRadio = isSelectKey && focusedRadio && radios.includes(focusedRadio) ? focusedRadio : checkedRadio;
|
|
178
|
+
const incr = isSelectKey ? 0 : ['ArrowUp', 'ArrowLeft'].includes(event.key) ? -1 : 1;
|
|
179
|
+
let index = radios.indexOf(fromRadio) + incr;
|
|
162
180
|
|
|
163
181
|
if (index < 0) {
|
|
164
182
|
index = radios.length - 1;
|
|
@@ -172,10 +190,14 @@ export default class ZnRadioGroup extends ZincElement implements ZincFormControl
|
|
|
172
190
|
radio.checked = false;
|
|
173
191
|
});
|
|
174
192
|
|
|
193
|
+
const oldValue = this.value;
|
|
175
194
|
this.value = radios[index].value;
|
|
176
195
|
radios[index].checked = true;
|
|
177
|
-
radios[index].
|
|
196
|
+
radios[index].focus();
|
|
178
197
|
|
|
198
|
+
if (this.value !== oldValue) {
|
|
199
|
+
this.emit('zn-change');
|
|
200
|
+
}
|
|
179
201
|
|
|
180
202
|
event.preventDefault();
|
|
181
203
|
}
|
|
@@ -204,12 +226,22 @@ export default class ZnRadioGroup extends ZincElement implements ZincFormControl
|
|
|
204
226
|
if (this.square) {
|
|
205
227
|
radio.square = true;
|
|
206
228
|
}
|
|
229
|
+
|
|
230
|
+
if (this.centered) {
|
|
231
|
+
radio.centered = true;
|
|
232
|
+
}
|
|
207
233
|
})
|
|
208
234
|
);
|
|
235
|
+
this.syncScale();
|
|
236
|
+
}
|
|
209
237
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
238
|
+
private syncScale() {
|
|
239
|
+
const radios = this.getAllRadios();
|
|
240
|
+
const checkedIndex = radios.findIndex(radio => radio.value === this.value);
|
|
241
|
+
radios.forEach((radio, index) => {
|
|
242
|
+
radio.scale = this.scale;
|
|
243
|
+
radio.filled = this.scale && checkedIndex >= 0 && index <= checkedIndex;
|
|
244
|
+
});
|
|
213
245
|
}
|
|
214
246
|
|
|
215
247
|
private syncRadios() {
|
|
@@ -228,6 +260,7 @@ export default class ZnRadioGroup extends ZincElement implements ZincFormControl
|
|
|
228
260
|
private updateCheckedRadio() {
|
|
229
261
|
const radios = this.getAllRadios();
|
|
230
262
|
radios.forEach(radio => (radio.checked = radio.value === this.value));
|
|
263
|
+
this.syncScale();
|
|
231
264
|
this.formControlController.setValidity(this.validity?.valid);
|
|
232
265
|
}
|
|
233
266
|
|
|
@@ -236,6 +269,11 @@ export default class ZnRadioGroup extends ZincElement implements ZincFormControl
|
|
|
236
269
|
this.syncRadios();
|
|
237
270
|
}
|
|
238
271
|
|
|
272
|
+
@watch('scale', {waitUntilFirstUpdate: true})
|
|
273
|
+
handleScaleChange() {
|
|
274
|
+
this.syncRadios();
|
|
275
|
+
}
|
|
276
|
+
|
|
239
277
|
@watch('value')
|
|
240
278
|
handleValueChange() {
|
|
241
279
|
if (this.hasUpdated) {
|
|
@@ -54,6 +54,23 @@
|
|
|
54
54
|
height: auto;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/* A scale is one row of equal segments, however many radios it has. */
|
|
58
|
+
:host([scale]) .form-control-input {
|
|
59
|
+
display: grid;
|
|
60
|
+
grid-auto-flow: column;
|
|
61
|
+
grid-auto-columns: minmax(0, 1fr);
|
|
62
|
+
// Wider than the checked segment's ring and its offset, so the ring never touches a neighbour.
|
|
63
|
+
gap: var(--zn-spacing-x-small);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:host([scale]) .form-control--has-label .form-control-input {
|
|
67
|
+
margin-top: var(--zn-spacing-small);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
:host([scale]) ::slotted(zn-radio) {
|
|
71
|
+
margin-top: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
.form-control {
|
|
58
75
|
position: relative;
|
|
59
76
|
border: none;
|
|
@@ -8,9 +8,9 @@ describe('<zn-radio-group>', () => {
|
|
|
8
8
|
expect(el).to.exist;
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
it('propagates contained and
|
|
11
|
+
it('propagates contained, square and centered to its radios', async () => {
|
|
12
12
|
const el = await fixture<HTMLElement>(html`
|
|
13
|
-
<zn-radio-group contained square>
|
|
13
|
+
<zn-radio-group contained square centered>
|
|
14
14
|
<zn-radio value="one">One</zn-radio>
|
|
15
15
|
<zn-radio value="two">Two</zn-radio>
|
|
16
16
|
</zn-radio-group>
|
|
@@ -20,6 +20,67 @@ describe('<zn-radio-group>', () => {
|
|
|
20
20
|
el.querySelectorAll('zn-radio').forEach(child => {
|
|
21
21
|
expect(child).to.have.attribute('contained');
|
|
22
22
|
expect(child).to.have.attribute('square');
|
|
23
|
+
expect(child).to.have.attribute('centered');
|
|
23
24
|
});
|
|
24
25
|
});
|
|
26
|
+
|
|
27
|
+
it('selects the next radio and emits zn-change on arrow key', async () => {
|
|
28
|
+
const el = await fixture<HTMLElement & { value: string }>(html`
|
|
29
|
+
<zn-radio-group value="one">
|
|
30
|
+
<zn-radio value="one">One</zn-radio>
|
|
31
|
+
<zn-radio value="two">Two</zn-radio>
|
|
32
|
+
</zn-radio-group>
|
|
33
|
+
`);
|
|
34
|
+
const radios = el.querySelectorAll<HTMLElement & { checked: boolean }>('zn-radio');
|
|
35
|
+
let changes = 0;
|
|
36
|
+
el.addEventListener('zn-change', () => changes++);
|
|
37
|
+
|
|
38
|
+
radios[0].dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true, composed: true}));
|
|
39
|
+
|
|
40
|
+
expect(el.value).to.equal('two');
|
|
41
|
+
expect(radios[1].checked).to.be.true;
|
|
42
|
+
expect(changes).to.equal(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('selects the focused radio on Enter', async () => {
|
|
46
|
+
const el = await fixture<HTMLElement & { value: string }>(html`
|
|
47
|
+
<zn-radio-group value="one">
|
|
48
|
+
<zn-radio value="one">One</zn-radio>
|
|
49
|
+
<zn-radio value="two">Two</zn-radio>
|
|
50
|
+
<zn-radio value="three">Three</zn-radio>
|
|
51
|
+
</zn-radio-group>
|
|
52
|
+
`);
|
|
53
|
+
const radios = el.querySelectorAll<HTMLElement & { checked: boolean }>('zn-radio');
|
|
54
|
+
let changes = 0;
|
|
55
|
+
el.addEventListener('zn-change', () => changes++);
|
|
56
|
+
|
|
57
|
+
radios[2].dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true, composed: true}));
|
|
58
|
+
|
|
59
|
+
expect(el.value).to.equal('three');
|
|
60
|
+
expect(radios[2].checked).to.be.true;
|
|
61
|
+
expect(changes).to.equal(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('fills a scale up to the checked radio and leaves it unanswered until one is picked', async () => {
|
|
65
|
+
const el = await fixture<HTMLElement & { value: string }>(html`
|
|
66
|
+
<zn-radio-group scale>
|
|
67
|
+
<zn-radio value="1">1</zn-radio>
|
|
68
|
+
<zn-radio value="2">2</zn-radio>
|
|
69
|
+
<zn-radio value="3">3</zn-radio>
|
|
70
|
+
</zn-radio-group>
|
|
71
|
+
`);
|
|
72
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
73
|
+
const radios = [...el.querySelectorAll<HTMLElement & { filled: boolean }>('zn-radio')];
|
|
74
|
+
|
|
75
|
+
expect(el.value).to.equal('');
|
|
76
|
+
radios.forEach(radio => {
|
|
77
|
+
expect(radio).to.have.attribute('scale');
|
|
78
|
+
expect(radio.filled).to.be.false;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
el.value = '2';
|
|
82
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
83
|
+
|
|
84
|
+
expect(radios.map(radio => radio.filled)).to.deep.equal([true, true, false]);
|
|
85
|
+
});
|
|
25
86
|
});
|
|
@@ -154,6 +154,27 @@ export default class ZnRating extends ZincElement implements ZincFormControl {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
private _handleKeyDown(event: KeyboardEvent) {
|
|
158
|
+
if (this.disabled || this.readonly) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const step = {ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1}[event.key];
|
|
163
|
+
let value: number;
|
|
164
|
+
if (event.key === 'Home') {
|
|
165
|
+
value = 0;
|
|
166
|
+
} else if (event.key === 'End') {
|
|
167
|
+
value = this.max;
|
|
168
|
+
} else if (step !== undefined) {
|
|
169
|
+
value = Math.min(Math.max(this.value + step * this.precision, 0), this.max);
|
|
170
|
+
} else {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
this.value = value;
|
|
176
|
+
}
|
|
177
|
+
|
|
157
178
|
private _handleClick(event: MouseEvent) {
|
|
158
179
|
if (this.readonly || this.disabled) {
|
|
159
180
|
return;
|
|
@@ -231,6 +252,8 @@ export default class ZnRating extends ZincElement implements ZincFormControl {
|
|
|
231
252
|
aria-valuenow="${this.value}"
|
|
232
253
|
aria-valuemin="0"
|
|
233
254
|
aria-valuemax="${this.max}"
|
|
255
|
+
tabindex="${this.disabled ? '-1' : '0'}"
|
|
256
|
+
@keydown="${this._handleKeyDown}"
|
|
234
257
|
>
|
|
235
258
|
<span
|
|
236
259
|
class="rating__symbols"
|
|
@@ -4,6 +4,7 @@ import {continueList, indentList, isEmptyListItem} from './list-continuation';
|
|
|
4
4
|
import {type CSSResultGroup, html, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
|
|
5
5
|
import {defaultValue} from "../../internal/default-value";
|
|
6
6
|
import {FormControlController} from "../../internal/form";
|
|
7
|
+
import {ifDefined} from "lit/directives/if-defined.js";
|
|
7
8
|
import {pastedRemarkd} from './paste';
|
|
8
9
|
import {property, query, state} from 'lit/decorators.js';
|
|
9
10
|
import {parse as remarkdParse} from "remarkd-js";
|
|
@@ -652,7 +653,11 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
652
653
|
}
|
|
653
654
|
}
|
|
654
655
|
|
|
655
|
-
private
|
|
656
|
+
private get editable() {
|
|
657
|
+
return !this.disabled && !this.readonly;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private handleRenderedClick(e: Event, index: number) {
|
|
656
661
|
const checkbox = (e.target as HTMLElement).closest<HTMLInputElement>('input[type="checkbox"]');
|
|
657
662
|
if (checkbox) {
|
|
658
663
|
// Toggle the task in the source rather than opening the editor.
|
|
@@ -791,7 +796,9 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
791
796
|
private handleImageControlsKeydown = (e: KeyboardEvent) => {
|
|
792
797
|
if (e.key === 'Escape') {
|
|
793
798
|
e.preventDefault();
|
|
799
|
+
const index = this.editingIndex;
|
|
794
800
|
this.closeImageEdit();
|
|
801
|
+
if (index !== null) void this.focusRenderedBlock(index);
|
|
795
802
|
} else if (e.key === 'Enter' && (e.target as HTMLElement).tagName === 'INPUT') {
|
|
796
803
|
e.preventDefault();
|
|
797
804
|
this.saveImageEdit();
|
|
@@ -992,13 +999,40 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
992
999
|
this.insertDraftBlock(next);
|
|
993
1000
|
} else if (e.key === 'Escape' || (e.key === 'Enter' && (e.metaKey || e.ctrlKey))) {
|
|
994
1001
|
e.preventDefault();
|
|
995
|
-
|
|
1002
|
+
this.suppressBlurCommit = true;
|
|
1003
|
+
const index = this.editingIndex ?? 0;
|
|
1004
|
+
this.commitEdit();
|
|
1005
|
+
void this.focusRenderedBlock(index);
|
|
996
1006
|
} else if (e.key === 'Backspace' && input.value === '') {
|
|
997
1007
|
e.preventDefault();
|
|
998
|
-
|
|
1008
|
+
this.suppressBlurCommit = true;
|
|
1009
|
+
const index = this.editingIndex ?? 0;
|
|
1010
|
+
this.commitEdit();
|
|
1011
|
+
void this.focusRenderedBlock(index - 1);
|
|
999
1012
|
}
|
|
1000
1013
|
};
|
|
1001
1014
|
|
|
1015
|
+
/** Focuses the rendered block at `index`, clamped to the blocks there are; the add row when there are none. */
|
|
1016
|
+
private async focusRenderedBlock(index: number) {
|
|
1017
|
+
await this.updateComplete;
|
|
1018
|
+
this.suppressBlurCommit = false;
|
|
1019
|
+
const blocks = this.shadowRoot?.querySelectorAll('.remarkd-editor__block') ?? [];
|
|
1020
|
+
const block = blocks[Math.max(0, Math.min(index, blocks.length - 1))];
|
|
1021
|
+
const target = block?.querySelector<HTMLElement>(':scope > .remarkd-editor__rendered')
|
|
1022
|
+
?? this.shadowRoot?.querySelector<HTMLElement>('.remarkd-editor__add');
|
|
1023
|
+
target?.focus({preventScroll: true});
|
|
1024
|
+
this.reveal(target);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
private moveBlock(from: number, to: number) {
|
|
1028
|
+
if (!this.editable || to < 0 || to >= this.blocks.length) return;
|
|
1029
|
+
const blocks = [...this.blocks];
|
|
1030
|
+
const [moved] = blocks.splice(from, 1);
|
|
1031
|
+
blocks.splice(to, 0, moved);
|
|
1032
|
+
this.updateBlocks(blocks);
|
|
1033
|
+
void this.focusRenderedBlock(to);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1002
1036
|
/**
|
|
1003
1037
|
* The draft to commit when Enter should finish the block — pressed on a trailing blank line or
|
|
1004
1038
|
* empty list item, outside any container — with that line dropped. Null otherwise.
|
|
@@ -1865,7 +1899,19 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1865
1899
|
// its own inner body div carries the marker instead, see renderConditional.
|
|
1866
1900
|
'remarkd-editor__rendered--parsed': !include && !conditional && !meta,
|
|
1867
1901
|
})}
|
|
1868
|
-
|
|
1902
|
+
tabindex=${ifDefined(this.editable ? '0' : undefined)}
|
|
1903
|
+
@click=${(e: MouseEvent) => this.handleRenderedClick(e, index)}
|
|
1904
|
+
@keydown=${(e: KeyboardEvent) => {
|
|
1905
|
+
// Links and task checkboxes inside the block keep their own keys
|
|
1906
|
+
if (e.target !== e.currentTarget) return;
|
|
1907
|
+
if (e.altKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
|
1908
|
+
e.preventDefault();
|
|
1909
|
+
this.moveBlock(index, index + (e.key === 'ArrowUp' ? -1 : 1));
|
|
1910
|
+
} else if (e.key === 'Enter') {
|
|
1911
|
+
e.preventDefault();
|
|
1912
|
+
this.handleRenderedClick(e, index);
|
|
1913
|
+
}
|
|
1914
|
+
}}>${
|
|
1869
1915
|
include ? this.renderIncludeChip(include)
|
|
1870
1916
|
: conditional ? conditional
|
|
1871
1917
|
: meta ? this.renderMetaChips(meta)
|
|
@@ -2012,7 +2058,16 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
2012
2058
|
})}>
|
|
2013
2059
|
${this.rawMode ? this.renderRaw() : html`
|
|
2014
2060
|
${this.renderBody()}
|
|
2015
|
-
<div class="remarkd-editor__add"
|
|
2061
|
+
<div class="remarkd-editor__add"
|
|
2062
|
+
role=${ifDefined(this.editable ? 'button' : undefined)}
|
|
2063
|
+
tabindex=${ifDefined(this.editable ? '0' : undefined)}
|
|
2064
|
+
aria-label=${ifDefined(this.editable ? 'Add block' : undefined)}
|
|
2065
|
+
@click=${() => this.insertDraftBlock(this.blocks.length)}
|
|
2066
|
+
@keydown=${(e: KeyboardEvent) => {
|
|
2067
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
2068
|
+
e.preventDefault();
|
|
2069
|
+
this.insertDraftBlock(this.blocks.length);
|
|
2070
|
+
}}>
|
|
2016
2071
|
${this.blocks.length === 0 && this.editingIndex === null ? this.placeholder : ''}
|
|
2017
2072
|
</div>`}
|
|
2018
2073
|
</div>
|
|
@@ -187,6 +187,14 @@
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
.remarkd-editor__rendered,
|
|
191
|
+
.remarkd-editor__add {
|
|
192
|
+
&:focus-visible {
|
|
193
|
+
outline: var(--zn-focus-ring);
|
|
194
|
+
border-radius: var(--zn-border-radius);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
190
198
|
// The shell keeps the block's rendered chrome while its source is edited —
|
|
191
199
|
// remarkd hint classes (hint-note, hint-tip, …) apply their own styling here.
|
|
192
200
|
.remarkd-editor__edit-shell {
|
|
@@ -248,10 +256,10 @@
|
|
|
248
256
|
}
|
|
249
257
|
|
|
250
258
|
.remarkd-editor__edit-shell--code {
|
|
251
|
-
background: var(--rmd-code-bg, #0f1626);
|
|
259
|
+
background: var(--rmd-code-bg, var(--zn-code-background, #0f1626));
|
|
252
260
|
|
|
253
261
|
.remarkd-editor__input {
|
|
254
|
-
color: var(--rmd-code-text, #f5f8ff);
|
|
262
|
+
color: var(--rmd-code-text, var(--zn-code-color, #f5f8ff));
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
|
|
@@ -299,7 +307,7 @@
|
|
|
299
307
|
align-items: center;
|
|
300
308
|
gap: 4px;
|
|
301
309
|
margin-bottom: var(--zn-spacing-x-small);
|
|
302
|
-
color: rgb(var(--zn-primary));
|
|
310
|
+
color: var(--zn-color-primary-fg, rgb(var(--zn-primary)));
|
|
303
311
|
font-size: 0.75rem;
|
|
304
312
|
text-transform: uppercase;
|
|
305
313
|
letter-spacing: 0.04em;
|
|
@@ -328,7 +336,7 @@
|
|
|
328
336
|
padding: 0 2px;
|
|
329
337
|
border-radius: 3px;
|
|
330
338
|
background: rgba(var(--zn-primary), 0.12);
|
|
331
|
-
color: rgb(var(--zn-primary));
|
|
339
|
+
color: var(--zn-color-primary-fg, rgb(var(--zn-primary)));
|
|
332
340
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
333
341
|
}
|
|
334
342
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import ZnReportTable from './report-table.component';
|
|
2
|
+
|
|
3
|
+
export * from './report-table.component';
|
|
4
|
+
export default ZnReportTable;
|
|
5
|
+
|
|
6
|
+
ZnReportTable.define('zn-report-table');
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface HTMLElementTagNameMap {
|
|
10
|
+
'zn-report-table': ZnReportTable;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {expect} from '@open-wc/testing';
|
|
2
|
+
import {formatValue, toneClass, unitLabel} from './report-format';
|
|
3
|
+
import type {ReportColumn} from './report-math';
|
|
4
|
+
|
|
5
|
+
const col = (c: Partial<ReportColumn>): ReportColumn => ({key: 'k', label: 'K', ...c});
|
|
6
|
+
const opts = {currency: 'USD', locale: 'en-US'};
|
|
7
|
+
|
|
8
|
+
describe('formatValue', () => {
|
|
9
|
+
it('formats currency with bracketed negatives', () => {
|
|
10
|
+
const c = col({format: 'currency'});
|
|
11
|
+
expect(formatValue(c, -1396.73, opts)).to.equal('($1,396.73)');
|
|
12
|
+
expect(formatValue(c, 3000.39, opts)).to.equal('$3,000.39');
|
|
13
|
+
expect(formatValue(c, -0.0001, opts)).to.equal('$0.00');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('shows the narrow currency symbol in any locale', () => {
|
|
17
|
+
const c = col({format: 'currency'});
|
|
18
|
+
expect(formatValue(c, 3000.39, {currency: 'USD', locale: 'en-GB'})).to.equal('$3,000.39');
|
|
19
|
+
expect(formatValue(c, -1396.73, {currency: 'USD', locale: 'en-GB'})).to.equal('($1,396.73)');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('formats financial in decimal style', () => {
|
|
23
|
+
expect(formatValue(col({format: 'financial'}), -1396.73, opts)).to.equal('(1,396.73)');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('formats numbers with a leading minus', () => {
|
|
27
|
+
expect(formatValue(col({format: 'number'}), -32128, opts)).to.equal('-32,128');
|
|
28
|
+
expect(formatValue(col({format: 'number'}), -0.2, opts)).to.equal('0');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('formats durations in seconds as hours, minutes and seconds', () => {
|
|
32
|
+
expect(formatValue(col({format: 'duration'}), 5400, opts)).to.equal('01:30:00');
|
|
33
|
+
expect(formatValue(col({format: 'duration'}), 59, opts)).to.equal('00:00:59');
|
|
34
|
+
expect(formatValue(col({format: 'duration'}), 453906, opts)).to.equal('126:05:06');
|
|
35
|
+
expect(formatValue(col({format: 'duration'}), -3600, opts)).to.equal('-01:00:00');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('labels the unit of durations, currency and percent', () => {
|
|
39
|
+
expect(unitLabel(col({format: 'duration'}), 'USD')).to.equal('hh:mm:ss');
|
|
40
|
+
expect(unitLabel(col({format: 'currency'}), 'USD')).to.equal('USD');
|
|
41
|
+
expect(unitLabel(col({format: 'currency', currency: 'GBP'}), 'USD')).to.equal('GBP');
|
|
42
|
+
expect(unitLabel(col({format: 'percent'}), 'USD')).to.equal('%');
|
|
43
|
+
expect(unitLabel(col({format: 'number'}), 'USD')).to.equal('');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('formats percent', () => {
|
|
47
|
+
expect(formatValue(col({format: 'percent', decimals: 2}), -0.0157, opts)).to.equal('-1.57%');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('defaults to number format with 0 decimals', () => {
|
|
51
|
+
expect(formatValue(col({}), 1234.5, opts)).to.equal('1,235');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('shows no sign on rounded zeros for financial and percent', () => {
|
|
55
|
+
expect(formatValue(col({format: 'financial'}), -0.0001, opts)).to.equal('0.00');
|
|
56
|
+
expect(formatValue(col({format: 'percent'}), -0.00001, opts)).to.equal('0.00%');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('honours the column currency override', () => {
|
|
60
|
+
expect(formatValue(col({format: 'currency', currency: 'EUR'}), 5, opts)).to.contain('€');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('handles null and text', () => {
|
|
64
|
+
expect(formatValue(col({format: 'number'}), null, opts)).to.equal('–');
|
|
65
|
+
expect(formatValue(col({format: 'text'}), 'abc', opts)).to.equal('abc');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('toneClass', () => {
|
|
70
|
+
it('colours breakeven columns', () => {
|
|
71
|
+
expect(toneClass(col({style: 'breakeven'}), 5)).to.equal('report__pos');
|
|
72
|
+
expect(toneClass(col({style: 'breakeven'}), -5)).to.equal('report__neg');
|
|
73
|
+
expect(toneClass(col({style: 'ibreakeven'}), 5)).to.equal('report__neg');
|
|
74
|
+
expect(toneClass(col({style: 'none'}), -5)).to.equal('');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('scales percent values to display units', () => {
|
|
78
|
+
const c = col({style: 'breakeven', format: 'percent'});
|
|
79
|
+
expect(toneClass(c, 0.004)).to.equal('report__pos');
|
|
80
|
+
expect(toneClass(c, -0.00004)).to.equal('');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('ignores values that round to the threshold', () => {
|
|
84
|
+
expect(toneClass(col({style: 'breakeven', format: 'currency'}), -0.0001)).to.equal('');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type {ReportColumn} from './report-math';
|
|
2
|
+
|
|
3
|
+
export function displayDecimals(column: ReportColumn): number {
|
|
4
|
+
return column.decimals ?? ((column.format ?? 'number') === 'number' ? 0 : 2);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function round(value: number, decimals: number): number {
|
|
8
|
+
const rounded = Number(value.toFixed(decimals));
|
|
9
|
+
return rounded === 0 ? 0 : rounded;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatValue(column: ReportColumn, value: number | string | null, opts: {currency: string; locale?: string}): string {
|
|
13
|
+
if (value === null || value === undefined) {
|
|
14
|
+
return '–';
|
|
15
|
+
}
|
|
16
|
+
const format = column.format ?? 'number';
|
|
17
|
+
if (format === 'text' || typeof value === 'string') {
|
|
18
|
+
return String(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (format === 'duration') {
|
|
22
|
+
const seconds = Math.round(Math.abs(value));
|
|
23
|
+
const pad = (part: number) => String(part).padStart(2, '0');
|
|
24
|
+
const text = `${pad(Math.floor(seconds / 3600))}:${pad(Math.floor(seconds / 60) % 60)}:${pad(seconds % 60)}`;
|
|
25
|
+
return value < 0 && seconds > 0 ? `-${text}` : text;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const decimals = displayDecimals(column);
|
|
29
|
+
const scaled = format === 'percent' ? value * 100 : value;
|
|
30
|
+
const rounded = round(scaled, decimals);
|
|
31
|
+
const digits = {minimumFractionDigits: decimals, maximumFractionDigits: decimals};
|
|
32
|
+
const abs = Math.abs(rounded);
|
|
33
|
+
|
|
34
|
+
if (format === 'currency' || format === 'financial') {
|
|
35
|
+
const style = format === 'currency'
|
|
36
|
+
? {style: 'currency' as const, currency: column.currency ?? opts.currency, currencyDisplay: 'narrowSymbol' as const}
|
|
37
|
+
: {style: 'decimal' as const};
|
|
38
|
+
const text = new Intl.NumberFormat(opts.locale, {...style, ...digits}).format(abs);
|
|
39
|
+
return rounded < 0 ? `(${text})` : text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const text = new Intl.NumberFormat(opts.locale, {style: 'decimal', ...digits}).format(abs);
|
|
43
|
+
const signed = rounded < 0 ? `-${text}` : text;
|
|
44
|
+
return format === 'percent' ? `${signed}%` : signed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function unitLabel(column: ReportColumn, currency: string): string {
|
|
48
|
+
switch (column.format) {
|
|
49
|
+
case 'duration':
|
|
50
|
+
return 'hh:mm:ss';
|
|
51
|
+
case 'currency':
|
|
52
|
+
return column.currency ?? currency;
|
|
53
|
+
case 'percent':
|
|
54
|
+
return '%';
|
|
55
|
+
default:
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function toneClass(column: ReportColumn, value: number | null): 'report__pos' | 'report__neg' | '' {
|
|
61
|
+
if ((column.style !== 'breakeven' && column.style !== 'ibreakeven') || value === null || value === undefined) {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
const decimals = displayDecimals(column);
|
|
65
|
+
const scale = column.format === 'percent' ? 100 : 1;
|
|
66
|
+
const diff = round(value * scale, decimals) - round((column.threshold ?? 0) * scale, decimals);
|
|
67
|
+
if (round(diff, decimals) === 0) {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
const positive = diff > 0;
|
|
71
|
+
return positive === (column.style === 'breakeven') ? 'report__pos' : 'report__neg';
|
|
72
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {aggregate, columnValue, compileFormula, computeTotals} from './report-math';
|
|
2
|
+
import {expect} from '@open-wc/testing';
|
|
3
|
+
import type { ReportColumn, ReportRow, ReportValues} from './report-math';
|
|
4
|
+
|
|
5
|
+
describe('report-math', () => {
|
|
6
|
+
it('evaluates arithmetic with precedence, parentheses, unary minus and a leading =', () => {
|
|
7
|
+
expect(compileFormula('1 + 2 * 3')({})).to.equal(7);
|
|
8
|
+
expect(compileFormula('(1 + 2) * 3')({})).to.equal(9);
|
|
9
|
+
expect(compileFormula('-{a} + 4')({a: 1})).to.equal(3);
|
|
10
|
+
expect(compileFormula('=10 / 4')({})).to.equal(2.5);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('resolves placeholders to 0 when missing, zero-divided or non-numeric, and parses numeric strings', () => {
|
|
14
|
+
expect(compileFormula('{a} / {b}')({a: 5, b: 0})).to.equal(0);
|
|
15
|
+
expect(compileFormula('{x.y.z}')({})).to.equal(0);
|
|
16
|
+
expect(compileFormula('{a}')({a: '12.5'})).to.equal(12.5);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('evaluates min and max with any number of arguments', () => {
|
|
20
|
+
expect(compileFormula('max(0, {a} - {b})')({a: 1, b: 5})).to.equal(0);
|
|
21
|
+
expect(compileFormula('min(1, {a} / {b})')({a: 9, b: 3})).to.equal(1);
|
|
22
|
+
expect(compileFormula('MAX(1, 2, 3)')({})).to.equal(3);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('throws SyntaxError on bad syntax', () => {
|
|
26
|
+
expect(() => compileFormula('1 +')).to.throw(SyntaxError);
|
|
27
|
+
expect(() => compileFormula('{a')).to.throw(SyntaxError);
|
|
28
|
+
expect(() => compileFormula('sum(1, 2)')).to.throw(SyntaxError);
|
|
29
|
+
expect(() => compileFormula('max 1')).to.throw(SyntaxError);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('shows 0 for an invalid formula and warns once', () => {
|
|
33
|
+
const original = console.warn;
|
|
34
|
+
const warnings: unknown[] = [];
|
|
35
|
+
console.warn = (...args: unknown[]) => warnings.push(args);
|
|
36
|
+
try {
|
|
37
|
+
const column: ReportColumn = {key: 'x', label: 'X', formula: '{a} * (2'};
|
|
38
|
+
expect(columnValue(column, {a: 3})).to.equal(0);
|
|
39
|
+
expect(columnValue(column, {a: 4})).to.equal(0);
|
|
40
|
+
expect(warnings).to.have.length(1);
|
|
41
|
+
expect(() => compileFormula('{a} * (2')).to.throw(SyntaxError);
|
|
42
|
+
} finally {
|
|
43
|
+
console.warn = original;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('aggregates numeric leaves by path and skips non-numeric strings', () => {
|
|
48
|
+
expect(aggregate([
|
|
49
|
+
{d1: {i: {unique: 2}}, label: 'x'},
|
|
50
|
+
{d1: {i: {unique: 3}}, d2: {total: {revenue: '1.5'}}},
|
|
51
|
+
])).to.deep.equal({d1: {i: {unique: 5}}, d2: {total: {revenue: 1.5}}});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('ignores prototype-polluting keys when aggregating', () => {
|
|
55
|
+
aggregate([JSON.parse('{"__proto__": {"polluted": 1}}') as ReportValues]);
|
|
56
|
+
expect(({} as Record<string, unknown>).polluted).to.be.undefined;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('recalculates ratio totals from summed inputs', () => {
|
|
60
|
+
const columns: ReportColumn[] = [{key: 'r', label: 'R', format: 'percent', formula: '{a} / {b}'}];
|
|
61
|
+
const rows: ReportRow[] = [{a: 1, b: 10}, {a: 9, b: 10}, {a: 0, b: 80}].map((values, i) => ({id: `${i}`, label: `${i}`, values}));
|
|
62
|
+
expect(computeTotals(columns, rows).r).to.equal(0.1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns null totals for non-summable and text columns', () => {
|
|
66
|
+
const columns: ReportColumn[] = [
|
|
67
|
+
{key: 'a', label: 'A', summable: false},
|
|
68
|
+
{key: 'b', label: 'B', format: 'text'},
|
|
69
|
+
];
|
|
70
|
+
const totals = computeTotals(columns, [{id: '1', label: '1', values: {a: 1, b: 2}}]);
|
|
71
|
+
expect(totals.a).to.be.null;
|
|
72
|
+
expect(totals.b).to.be.null;
|
|
73
|
+
});
|
|
74
|
+
});
|