@kubex/zinc 1.1.184 → 2.0.0
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 +10344 -2583
- package/dist/vscode.html-custom-data.json +1102 -125
- package/dist/web-types.json +2217 -224
- package/dist/zn.d.ts +2306 -417
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +2313 -1635
- 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/filter-wrapper.md +20 -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 +242 -11
- package/src/components/button/button.scss +133 -25
- 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 +17 -3
- package/src/components/cols/cols.scss +24 -13
- package/src/components/cols/cols.test.ts +33 -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 +364 -142
- package/src/components/data-table/data-table.scss +133 -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/filter-wrapper/filter-wrapper.component.ts +45 -10
- package/src/components/filter-wrapper/filter-wrapper.test.ts +58 -2
- 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 +625 -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
|
@@ -186,6 +186,8 @@ export default class ZnDataTableSearch extends ZincElement implements ZincFormCo
|
|
|
186
186
|
<div class="data-table-search">
|
|
187
187
|
<zn-input
|
|
188
188
|
type="search"
|
|
189
|
+
size="small"
|
|
190
|
+
autocomplete="off"
|
|
189
191
|
name="${this.name}"
|
|
190
192
|
placeholder="${this.placeholder}"
|
|
191
193
|
help-text="${this.helpText}"
|
|
@@ -193,11 +195,11 @@ export default class ZnDataTableSearch extends ZincElement implements ZincFormCo
|
|
|
193
195
|
clearable
|
|
194
196
|
@zn-input="${this.handleInput}"
|
|
195
197
|
@zn-clear="${this.handleClear}">
|
|
198
|
+
<zn-icon slot="prefix" src="search@lu" size="16"></zn-icon>
|
|
196
199
|
${this.helpText ? html`
|
|
197
200
|
<zn-tooltip slot="suffix" class="data-table-search__tooltip" content="${this.helpText}">
|
|
198
|
-
<zn-icon src="info@lu" size="
|
|
201
|
+
<zn-icon src="info@lu" size="16"></zn-icon>
|
|
199
202
|
</zn-tooltip>` : nothing}
|
|
200
|
-
<zn-icon slot="suffix" src="search@lu" size="20"></zn-icon>
|
|
201
203
|
</zn-input>
|
|
202
204
|
<slot style="display: none"></slot>
|
|
203
205
|
</div>
|
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
:host {
|
|
4
4
|
display: block;
|
|
5
|
+
min-width: 0;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
.data-table-search {
|
|
9
|
+
width: 100%;
|
|
8
10
|
display: flex;
|
|
9
11
|
align-items: center;
|
|
10
12
|
gap: var(--zn-spacing-small);
|
|
11
|
-
min-width:
|
|
13
|
+
min-width: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
zn-input {
|
|
17
|
+
flex: 1;
|
|
18
|
+
min-width: 0;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
// Keeps the focus ring inside the component's own box, so a toolbar or header that
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
2
|
import {expect, fixture, html} from '@open-wc/testing';
|
|
3
3
|
import type ZnDataTableSearch from './data-table-search.component';
|
|
4
|
+
import type ZnInput from '../input/input.component';
|
|
4
5
|
import type ZnSelect from '../select/select.component';
|
|
5
6
|
|
|
6
7
|
describe('<zn-data-table-search>', () => {
|
|
@@ -32,4 +33,32 @@ describe('<zn-data-table-search>', () => {
|
|
|
32
33
|
|
|
33
34
|
expect(el.getFormData().status).to.equal('closed');
|
|
34
35
|
});
|
|
36
|
+
|
|
37
|
+
it('puts the search icon in the prefix slot, before the input', async () => {
|
|
38
|
+
const el = await fixture<ZnDataTableSearch>(html`<zn-data-table-search></zn-data-table-search>`);
|
|
39
|
+
const input = el.shadowRoot!.querySelector<HTMLElement>('zn-input')!;
|
|
40
|
+
const icon = input.querySelector<HTMLElement>('zn-icon[slot="prefix"]')!;
|
|
41
|
+
expect(icon).to.exist;
|
|
42
|
+
expect(input.querySelector('zn-icon[slot="suffix"]')).to.not.exist;
|
|
43
|
+
await (input as unknown as ZnInput).updateComplete;
|
|
44
|
+
const control = input.shadowRoot!.querySelector('input')!;
|
|
45
|
+
expect(icon.getBoundingClientRect().right).to.be.lessThanOrEqual(control.getBoundingClientRect().left + 1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('can shrink below 200px in a narrow container', async () => {
|
|
49
|
+
const wrap = await fixture<HTMLElement>(html`
|
|
50
|
+
<div style="display: flex; width: 120px"><zn-data-table-search></zn-data-table-search></div>`);
|
|
51
|
+
const el = wrap.querySelector<ZnDataTableSearch>('zn-data-table-search')!;
|
|
52
|
+
await el.updateComplete;
|
|
53
|
+
expect(getComputedStyle(el).minWidth).to.not.equal('200px');
|
|
54
|
+
expect(el.getBoundingClientRect().width).to.be.at.most(120);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('turns off the browser autofill history on the native input', async () => {
|
|
58
|
+
const el = await fixture<ZnDataTableSearch>(html`<zn-data-table-search></zn-data-table-search>`);
|
|
59
|
+
const input = el.shadowRoot!.querySelector('zn-input') as unknown as ZnInput;
|
|
60
|
+
await input.updateComplete;
|
|
61
|
+
|
|
62
|
+
expect(input.shadowRoot!.querySelector('input')!.getAttribute('autocomplete')).to.equal('off');
|
|
63
|
+
});
|
|
35
64
|
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {addMonths, clampRange, defaultPresets, isIso, monthGrid, pick, rangeLabel, todayIso} from './date-range-math';
|
|
2
|
+
import {expect} from '@open-wc/testing';
|
|
3
|
+
|
|
4
|
+
describe('date-range-math', () => {
|
|
5
|
+
it('moves by months, clamping to the shorter month', () => {
|
|
6
|
+
expect(addMonths('2026-01-31', 1)).to.equal('2026-02-28');
|
|
7
|
+
expect(addMonths('2026-03-15', -1)).to.equal('2026-02-15');
|
|
8
|
+
expect(addMonths('2026-12-10', 1)).to.equal('2027-01-10');
|
|
9
|
+
expect(addMonths('2024-02-29', 12)).to.equal('2025-02-28');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('lists the eight quick ranges relative to today', () => {
|
|
13
|
+
const presets = defaultPresets('2026-10-20');
|
|
14
|
+
const byLabel = Object.fromEntries(presets.map(p => [p.label, {from: p.from, to: p.to}]));
|
|
15
|
+
|
|
16
|
+
expect(presets.map(p => p.label)).to.deep.equal([
|
|
17
|
+
'Today', 'Yesterday', 'Last 7 days', 'Last 14 days', 'Last 30 days', 'This month', 'Last month', 'Last 90 days'
|
|
18
|
+
]);
|
|
19
|
+
expect(byLabel['Today']).to.deep.equal({from: '2026-10-20', to: '2026-10-20'});
|
|
20
|
+
expect(byLabel['Yesterday']).to.deep.equal({from: '2026-10-19', to: '2026-10-19'});
|
|
21
|
+
expect(byLabel['Last 7 days']).to.deep.equal({from: '2026-10-13', to: '2026-10-20'});
|
|
22
|
+
expect(byLabel['Last 90 days']).to.deep.equal({from: '2026-07-22', to: '2026-10-20'});
|
|
23
|
+
expect(byLabel['This month']).to.deep.equal({from: '2026-10-01', to: '2026-10-20'});
|
|
24
|
+
expect(byLabel['Last month']).to.deep.equal({from: '2026-09-01', to: '2026-09-30'});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('handles last month across a year boundary', () => {
|
|
28
|
+
const lastMonth = defaultPresets('2026-01-15').find(p => p.label === 'Last month');
|
|
29
|
+
expect(lastMonth).to.deep.include({from: '2025-12-01', to: '2025-12-31'});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('builds a 42-cell Monday-first month grid', () => {
|
|
33
|
+
const grid = monthGrid(2026, 9);
|
|
34
|
+
expect(grid).to.have.length(42);
|
|
35
|
+
expect(grid.slice(0, 3)).to.deep.equal([null, null, null]);
|
|
36
|
+
expect(grid[3]).to.equal('2026-10-01');
|
|
37
|
+
expect(grid[33]).to.equal('2026-10-31');
|
|
38
|
+
expect(grid[34]).to.equal(null);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('formats a range label, or a single date when both ends match', () => {
|
|
42
|
+
expect(rangeLabel('2026-10-19', '2026-10-20')).to.equal('19 Oct 2026 to 20 Oct 2026');
|
|
43
|
+
expect(rangeLabel('2026-10-20', '2026-10-20')).to.equal('20 Oct 2026');
|
|
44
|
+
expect(rangeLabel('2026-09-01', '2026-09-30')).to.equal('1 Sep 2026 to 30 Sep 2026');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('picks a start, then an end, and restarts on an earlier day or after a full range', () => {
|
|
48
|
+
let state = pick({from: '', to: ''}, '2026-10-10');
|
|
49
|
+
expect(state).to.deep.equal({from: '2026-10-10', to: ''});
|
|
50
|
+
|
|
51
|
+
state = pick(state, '2026-10-15');
|
|
52
|
+
expect(state).to.deep.equal({from: '2026-10-10', to: '2026-10-15'});
|
|
53
|
+
|
|
54
|
+
state = pick(state, '2026-10-12');
|
|
55
|
+
expect(state).to.deep.equal({from: '2026-10-12', to: ''});
|
|
56
|
+
|
|
57
|
+
state = pick(state, '2026-10-05');
|
|
58
|
+
expect(state).to.deep.equal({from: '2026-10-05', to: ''});
|
|
59
|
+
|
|
60
|
+
expect(pick({from: '2026-10-05', to: ''}, '2026-10-05')).to.deep.equal({from: '2026-10-05', to: '2026-10-05'});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('clamps a range to min and max, or returns null when it lies wholly outside', () => {
|
|
64
|
+
expect(clampRange({from: '2026-10-13', to: '2026-10-20'}, '', '2026-10-18')).to.deep.equal({from: '2026-10-13', to: '2026-10-18'});
|
|
65
|
+
expect(clampRange({from: '2026-10-13', to: '2026-10-20'}, '2026-10-15', '')).to.deep.equal({from: '2026-10-15', to: '2026-10-20'});
|
|
66
|
+
expect(clampRange({from: '2026-10-19', to: '2026-10-20'}, '', '2026-10-18')).to.equal(null);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('rejects partial years a date input reports while typing', () => {
|
|
70
|
+
expect(isIso('0202-10-20')).to.be.false;
|
|
71
|
+
expect(isIso('2026-02-30')).to.be.false;
|
|
72
|
+
expect(isIso('2026-10-20')).to.be.true;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('uses the local date for today', () => {
|
|
76
|
+
expect(todayIso(new Date(2026, 9, 20, 23, 59))).to.equal('2026-10-20');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export type Iso = string;
|
|
2
|
+
|
|
3
|
+
export interface DatePreset {
|
|
4
|
+
label: string;
|
|
5
|
+
from: Iso;
|
|
6
|
+
to: Iso;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface RangeState {
|
|
10
|
+
from: Iso | '';
|
|
11
|
+
to: Iso | '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
15
|
+
|
|
16
|
+
// Day arithmetic runs in UTC so DST changes can't shift a date
|
|
17
|
+
export function toUtc(iso: Iso): Date {
|
|
18
|
+
const [y, m, d] = iso.split('-').map(Number);
|
|
19
|
+
return new Date(Date.UTC(y, m - 1, d));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function fromUtc(date: Date): Iso {
|
|
23
|
+
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Years below 1000 are rejected: Chromium's date input reports partial years like 0202 while typing
|
|
27
|
+
export function isIso(value: unknown): value is Iso {
|
|
28
|
+
return typeof value === 'string' && /^[1-9]\d{3}-\d{2}-\d{2}$/.test(value) && fromUtc(toUtc(value)) === value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function addDays(iso: Iso, days: number): Iso {
|
|
32
|
+
const date = toUtc(iso);
|
|
33
|
+
date.setUTCDate(date.getUTCDate() + days);
|
|
34
|
+
return fromUtc(date);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The same day `months` away, clamped to the target month's last day. */
|
|
38
|
+
export function addMonths(iso: Iso, months: number): Iso {
|
|
39
|
+
const date = toUtc(iso);
|
|
40
|
+
const target = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));
|
|
41
|
+
const lastDay = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
|
|
42
|
+
target.setUTCDate(Math.min(date.getUTCDate(), lastDay));
|
|
43
|
+
return fromUtc(target);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function daysBetween(from: Iso, to: Iso): number {
|
|
47
|
+
return Math.round((toUtc(to).getTime() - toUtc(from).getTime()) / 86400000);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function todayIso(now: Date = new Date()): Iso {
|
|
51
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function defaultPresets(today: Iso): DatePreset[] {
|
|
55
|
+
const [y, m] = today.split('-').map(Number);
|
|
56
|
+
const lastDayPrev = fromUtc(new Date(Date.UTC(y, m - 1, 0)));
|
|
57
|
+
const days = (n: number) => ({label: `Last ${n} days`, from: addDays(today, -n), to: today});
|
|
58
|
+
|
|
59
|
+
return [
|
|
60
|
+
{label: 'Today', from: today, to: today},
|
|
61
|
+
{label: 'Yesterday', from: addDays(today, -1), to: addDays(today, -1)},
|
|
62
|
+
days(7),
|
|
63
|
+
days(14),
|
|
64
|
+
days(30),
|
|
65
|
+
{label: 'This month', from: `${today.slice(0, 7)}-01`, to: today},
|
|
66
|
+
{label: 'Last month', from: `${lastDayPrev.slice(0, 7)}-01`, to: lastDayPrev},
|
|
67
|
+
days(90),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function monthGrid(year: number, month0: number, weekStart = 1): (Iso | null)[] {
|
|
72
|
+
const first = new Date(Date.UTC(year, month0, 1));
|
|
73
|
+
const length = new Date(Date.UTC(year, month0 + 1, 0)).getUTCDate();
|
|
74
|
+
const lead = (first.getUTCDay() - weekStart + 7) % 7;
|
|
75
|
+
|
|
76
|
+
return Array.from({length: 42}, (_, i) => {
|
|
77
|
+
const day = i - lead + 1;
|
|
78
|
+
return day >= 1 && day <= length ? fromUtc(new Date(Date.UTC(year, month0, day))) : null;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fixed rather than Intl: ICU builds disagree on short months (en-GB gives "Sept" in Chromium)
|
|
83
|
+
const SHORT_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
84
|
+
|
|
85
|
+
export function formatDate(iso: Iso): string {
|
|
86
|
+
const [y, m, d] = iso.split('-').map(Number);
|
|
87
|
+
return `${d} ${SHORT_MONTHS[m - 1]} ${y}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function rangeLabel(from: Iso, to: Iso): string {
|
|
91
|
+
return from === to ? formatDate(from) : `${formatDate(from)} to ${formatDate(to)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function clampRange(range: {from: Iso; to: Iso}, min: Iso | '', max: Iso | ''): {from: Iso; to: Iso} | null {
|
|
95
|
+
const from = min && range.from < min ? min : range.from;
|
|
96
|
+
const to = max && range.to > max ? max : range.to;
|
|
97
|
+
return from <= to ? {from, to} : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function pick(state: RangeState, day: Iso): {from: Iso; to: Iso | ''} {
|
|
101
|
+
if (!state.from || state.to || day < state.from) {
|
|
102
|
+
return {from: day, to: ''};
|
|
103
|
+
}
|
|
104
|
+
return {from: state.from, to: day};
|
|
105
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addDays,
|
|
3
|
+
addMonths,
|
|
4
|
+
clampRange,
|
|
5
|
+
type DatePreset,
|
|
6
|
+
daysBetween,
|
|
7
|
+
defaultPresets,
|
|
8
|
+
formatDate,
|
|
9
|
+
isIso,
|
|
10
|
+
type Iso,
|
|
11
|
+
monthGrid,
|
|
12
|
+
pick,
|
|
13
|
+
rangeLabel,
|
|
14
|
+
todayIso,
|
|
15
|
+
toUtc
|
|
16
|
+
} from './date-range-math';
|
|
17
|
+
import {AnchoredPopoverController} from '../../internal/anchored-popover';
|
|
18
|
+
import {classMap} from 'lit/directives/class-map.js';
|
|
19
|
+
import {type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS} from 'lit';
|
|
20
|
+
import {property, state} from 'lit/decorators.js';
|
|
21
|
+
import ZincElement from '../../internal/zinc-element';
|
|
22
|
+
import ZnButton from '../button';
|
|
23
|
+
import ZnIcon from '../icon';
|
|
24
|
+
import ZnInput from '../input';
|
|
25
|
+
|
|
26
|
+
import styles from './date-range.scss';
|
|
27
|
+
|
|
28
|
+
const WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
|
29
|
+
const POP_WIDTH = 736;
|
|
30
|
+
const NARROW = '(max-width: 560px)';
|
|
31
|
+
const KEY_STEPS: Record<string, number> = {ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7};
|
|
32
|
+
|
|
33
|
+
const presetsConverter = {
|
|
34
|
+
fromAttribute: (value: string | null): DatePreset[] | undefined => {
|
|
35
|
+
try {
|
|
36
|
+
const parsed: unknown = value ? JSON.parse(value) : undefined;
|
|
37
|
+
return Array.isArray(parsed) ? parsed as DatePreset[] : undefined;
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
toAttribute: (value: DatePreset[] | undefined) => value ? JSON.stringify(value) : null,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @summary A button showing a date range that opens a two-month calendar with quick ranges.
|
|
47
|
+
* @documentation https://zinc.style/components/date-range
|
|
48
|
+
* @status experimental
|
|
49
|
+
* @since 1.0
|
|
50
|
+
*
|
|
51
|
+
* @dependency zn-button
|
|
52
|
+
* @dependency zn-icon
|
|
53
|
+
* @dependency zn-input
|
|
54
|
+
*
|
|
55
|
+
* @event zn-change - Emitted when a range is applied, with `{from, to}` as its detail.
|
|
56
|
+
*
|
|
57
|
+
* @csspart trigger - The button that opens the picker.
|
|
58
|
+
* @csspart popover - The picker popover.
|
|
59
|
+
*/
|
|
60
|
+
export default class ZnDateRange extends ZincElement {
|
|
61
|
+
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
62
|
+
static formAssociated = true;
|
|
63
|
+
static dependencies = {
|
|
64
|
+
'zn-button': ZnButton,
|
|
65
|
+
'zn-icon': ZnIcon,
|
|
66
|
+
'zn-input': ZnInput,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** The first day of the range, `YYYY-MM-DD`. Defaults to today. */
|
|
70
|
+
@property({reflect: true}) from = '';
|
|
71
|
+
|
|
72
|
+
/** The last day of the range, `YYYY-MM-DD`. Defaults to today. */
|
|
73
|
+
@property({reflect: true}) to = '';
|
|
74
|
+
|
|
75
|
+
/** The earliest day that can be picked. */
|
|
76
|
+
@property() min = '';
|
|
77
|
+
|
|
78
|
+
/** The latest day that can be picked. */
|
|
79
|
+
@property() max = '';
|
|
80
|
+
|
|
81
|
+
/** Quick ranges as a JSON array of `{label, from, to}`, replacing the default ones. */
|
|
82
|
+
@property({converter: presetsConverter}) presets: DatePreset[] | undefined;
|
|
83
|
+
|
|
84
|
+
/** A short line shown under the calendar, such as how the range is used. */
|
|
85
|
+
@property() note = '';
|
|
86
|
+
|
|
87
|
+
/** Submits `name[from]` and `name[to]` with the form. */
|
|
88
|
+
@property() name = '';
|
|
89
|
+
|
|
90
|
+
/** The trigger button's size. */
|
|
91
|
+
@property() size: 'small' | 'medium' | 'large' = 'small';
|
|
92
|
+
|
|
93
|
+
@state() private draft: {from: Iso | ''; to: Iso | ''} = {from: '', to: ''};
|
|
94
|
+
@state() private view = {year: 0, month0: 0};
|
|
95
|
+
@state() private focusDay: Iso = '';
|
|
96
|
+
private readonly internals: ElementInternals | null;
|
|
97
|
+
// Not `popover`: that is the native Popover API attribute on every HTMLElement
|
|
98
|
+
private readonly anchor = new AnchoredPopoverController(this, {
|
|
99
|
+
trigger: '.date-range__trigger',
|
|
100
|
+
popover: '.date-range__pop',
|
|
101
|
+
width: () => Math.min(POP_WIDTH, window.innerWidth * 0.94),
|
|
102
|
+
align: 'start',
|
|
103
|
+
onHide: reason => {
|
|
104
|
+
if (reason === 'escape') this.focusTrigger();
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
constructor() {
|
|
109
|
+
super();
|
|
110
|
+
this.internals = typeof this.attachInternals === 'function' ? this.attachInternals() : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
protected updated(changed: PropertyValues) {
|
|
114
|
+
if (changed.has('from') || changed.has('to') || changed.has('name')) {
|
|
115
|
+
this.syncFormValue();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private get rangeFrom(): Iso {
|
|
120
|
+
return isIso(this.from) ? this.from : todayIso();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private get rangeTo(): Iso {
|
|
124
|
+
return isIso(this.to) ? this.to : this.rangeFrom;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A class rather than a media query: WebKit doesn't re-evaluate media queries in adopted sheets on resize
|
|
128
|
+
private get narrow(): boolean {
|
|
129
|
+
return window.matchMedia(NARROW).matches;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private get bounds(): [Iso | '', Iso | ''] {
|
|
133
|
+
return [isIso(this.min) ? this.min : '', isIso(this.max) ? this.max : ''];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private syncFormValue() {
|
|
137
|
+
if (!this.internals) return;
|
|
138
|
+
if (!this.name) {
|
|
139
|
+
this.internals.setFormValue(null);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const data = new FormData();
|
|
143
|
+
data.append(`${this.name}[from]`, this.rangeFrom);
|
|
144
|
+
data.append(`${this.name}[to]`, this.rangeTo);
|
|
145
|
+
this.internals.setFormValue(data);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Opens the picker. */
|
|
149
|
+
async show() {
|
|
150
|
+
this.draft = {from: this.rangeFrom, to: this.rangeTo};
|
|
151
|
+
this.showMonth(this.draft.from);
|
|
152
|
+
this.focusDay = this.draft.from;
|
|
153
|
+
this.anchor.show();
|
|
154
|
+
await this.updateComplete;
|
|
155
|
+
this.shadowRoot?.querySelector<HTMLElement>('.date-range__day[tabindex="0"]')?.focus();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Closes the picker without applying. */
|
|
159
|
+
hide() {
|
|
160
|
+
this.anchor.hide();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private close() {
|
|
164
|
+
this.hide();
|
|
165
|
+
this.focusTrigger();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private toggle = () => {
|
|
169
|
+
if (this.anchor.open) {
|
|
170
|
+
this.hide();
|
|
171
|
+
} else {
|
|
172
|
+
void this.show();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
private presetRange(preset: DatePreset) {
|
|
177
|
+
return clampRange(preset, ...this.bounds);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private focusTrigger() {
|
|
181
|
+
const trigger = this.anchor.trigger();
|
|
182
|
+
(trigger?.shadowRoot?.querySelector<HTMLElement>('button, a') ?? trigger)?.focus();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private applyRange(range: {from: Iso | ''; to: Iso | ''}) {
|
|
186
|
+
if (!range.from || !range.to || this.isDisabled(range.from) || this.isDisabled(range.to)) return;
|
|
187
|
+
this.from = range.from;
|
|
188
|
+
this.to = range.to;
|
|
189
|
+
this.close();
|
|
190
|
+
// Dispatched directly: zn-change's registered type carries no detail
|
|
191
|
+
this.dispatchEvent(new CustomEvent('zn-change', {
|
|
192
|
+
bubbles: true,
|
|
193
|
+
composed: true,
|
|
194
|
+
detail: {from: this.from, to: this.to}
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private showMonth(iso: Iso) {
|
|
199
|
+
const [y, m] = iso.split('-').map(Number);
|
|
200
|
+
this.view = {year: y, month0: m - 1};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private shiftMonth(by: number) {
|
|
204
|
+
this.shiftTo(this.view.year, this.view.month0 + by);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private shiftTo(year: number, month0: number) {
|
|
208
|
+
const date = new Date(Date.UTC(year, month0, 1));
|
|
209
|
+
this.view = {year: date.getUTCFullYear(), month0: date.getUTCMonth()};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private isDisabled(iso: Iso): boolean {
|
|
213
|
+
const [min, max] = this.bounds;
|
|
214
|
+
return (!!min && iso < min) || (!!max && iso > max);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private choose(iso: Iso) {
|
|
218
|
+
if (this.isDisabled(iso)) return;
|
|
219
|
+
this.draft = pick(this.draft, iso);
|
|
220
|
+
this.focusDay = iso;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private visible(iso: Iso): boolean {
|
|
224
|
+
const months = this.narrow ? 1 : 2;
|
|
225
|
+
const first = Date.UTC(this.view.year, this.view.month0, 1);
|
|
226
|
+
const end = Date.UTC(this.view.year, this.view.month0 + months, 1);
|
|
227
|
+
const at = toUtc(iso).getTime();
|
|
228
|
+
return at >= first && at < end;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private dayKeydown = async (e: KeyboardEvent) => {
|
|
232
|
+
const current = (e.target as HTMLElement).dataset.date;
|
|
233
|
+
if (!current) return;
|
|
234
|
+
// Weeks start on Monday
|
|
235
|
+
const weekday = (toUtc(current).getUTCDay() + 6) % 7;
|
|
236
|
+
const pageBy = e.shiftKey ? 12 : 1;
|
|
237
|
+
const next = KEY_STEPS[e.key] ? addDays(current, KEY_STEPS[e.key])
|
|
238
|
+
: e.key === 'Home' ? addDays(current, -weekday)
|
|
239
|
+
: e.key === 'End' ? addDays(current, 6 - weekday)
|
|
240
|
+
: e.key === 'PageUp' ? addMonths(current, -pageBy)
|
|
241
|
+
: e.key === 'PageDown' ? addMonths(current, pageBy)
|
|
242
|
+
: null;
|
|
243
|
+
if (!next) return;
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
if (!this.visible(next)) {
|
|
246
|
+
// Backwards lands in the first visible month, forwards in the last
|
|
247
|
+
const lastVisible = next < current ? 0 : (this.narrow ? 0 : 1);
|
|
248
|
+
const at = toUtc(next);
|
|
249
|
+
this.shiftTo(at.getUTCFullYear(), at.getUTCMonth() - lastVisible);
|
|
250
|
+
}
|
|
251
|
+
this.focusDay = next;
|
|
252
|
+
await this.updateComplete;
|
|
253
|
+
this.shadowRoot?.querySelector<HTMLElement>(`[data-date="${next}"]`)?.focus();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// On commit (blur or Enter), not per keystroke: a date input reports each segment edit as a new valid value
|
|
257
|
+
private typed(end: 'from' | 'to', e: Event) {
|
|
258
|
+
const value = (e.target as ZnInput).value as string;
|
|
259
|
+
if (!isIso(value) || this.isDisabled(value)) return;
|
|
260
|
+
|
|
261
|
+
if (end === 'from') {
|
|
262
|
+
this.draft = this.draft.to && value <= this.draft.to ? {from: value, to: this.draft.to} : {from: value, to: ''};
|
|
263
|
+
} else {
|
|
264
|
+
this.draft = this.draft.from && value >= this.draft.from ? {from: this.draft.from, to: value} : {from: value, to: ''};
|
|
265
|
+
}
|
|
266
|
+
this.focusDay = value;
|
|
267
|
+
this.showMonth(value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private stop = (e: Event) => e.stopPropagation();
|
|
271
|
+
|
|
272
|
+
render() {
|
|
273
|
+
return html`
|
|
274
|
+
<zn-button class="date-range__trigger" part="trigger" panel-bg size="${this.size}" icon="calendar@lu"
|
|
275
|
+
icon-size="16" aria-haspopup="dialog" aria-expanded="${this.anchor.open}" @click="${this.toggle}">
|
|
276
|
+
${rangeLabel(this.rangeFrom, this.rangeTo)}
|
|
277
|
+
<zn-icon class="date-range__chevron" src="chevron-down@lu" size="14"></zn-icon>
|
|
278
|
+
</zn-button>
|
|
279
|
+
${this.anchor.open ? this.renderPopover() : nothing}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private renderPopover() {
|
|
283
|
+
const presets = this.presets ?? defaultPresets(todayIso());
|
|
284
|
+
const {from, to} = this.draft;
|
|
285
|
+
const next = new Date(Date.UTC(this.view.year, this.view.month0 + 1, 1));
|
|
286
|
+
|
|
287
|
+
return html`
|
|
288
|
+
<div class="${classMap({'date-range__pop': true, 'is-narrow': this.narrow})}" part="popover" role="dialog" aria-label="Date range"
|
|
289
|
+
style="${this.anchor.style}" @zn-change="${this.stop}" @zn-input="${this.stop}"
|
|
290
|
+
@zn-focus="${this.stop}" @zn-blur="${this.stop}">
|
|
291
|
+
<div class="date-range__presets" role="group" aria-label="Quick ranges">
|
|
292
|
+
${presets.map(preset => {
|
|
293
|
+
const range = this.presetRange(preset);
|
|
294
|
+
const active = !!range && range.from === from && range.to === to;
|
|
295
|
+
return html`
|
|
296
|
+
<button type="button" class="${classMap({'date-range__preset': true, 'is-active': active})}"
|
|
297
|
+
aria-pressed="${active}" aria-disabled="${!range}"
|
|
298
|
+
@click="${() => range && this.applyRange(range)}">${preset.label}</button>`;
|
|
299
|
+
})}
|
|
300
|
+
</div>
|
|
301
|
+
<div class="date-range__main">
|
|
302
|
+
<div class="date-range__inputs">
|
|
303
|
+
<zn-input class="date-range__from" type="date" label="From" .value="${from}"
|
|
304
|
+
min="${this.min || nothing}" max="${this.max || nothing}"
|
|
305
|
+
@zn-blur="${(e: Event) => this.typed('from', e)}"
|
|
306
|
+
@keydown="${(e: KeyboardEvent) => e.key === 'Enter' && this.typed('from', e)}"></zn-input>
|
|
307
|
+
<span class="date-range__dash">to</span>
|
|
308
|
+
<zn-input class="date-range__to" type="date" label="To" .value="${to}"
|
|
309
|
+
min="${this.min || nothing}" max="${this.max || nothing}"
|
|
310
|
+
@zn-blur="${(e: Event) => this.typed('to', e)}"
|
|
311
|
+
@keydown="${(e: KeyboardEvent) => e.key === 'Enter' && this.typed('to', e)}"></zn-input>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="date-range__months">
|
|
314
|
+
<zn-button class="date-range__nav date-range__nav--prev" icon="chevron-left@lu" icon-size="16"
|
|
315
|
+
icon-button plain aria-label="Previous month"
|
|
316
|
+
@click="${() => this.shiftMonth(-1)}"></zn-button>
|
|
317
|
+
${this.renderMonth(this.view.year, this.view.month0)}
|
|
318
|
+
${this.renderMonth(next.getUTCFullYear(), next.getUTCMonth())}
|
|
319
|
+
<zn-button class="date-range__nav date-range__nav--next" icon="chevron-right@lu" icon-size="16"
|
|
320
|
+
icon-button plain aria-label="Next month"
|
|
321
|
+
@click="${() => this.shiftMonth(1)}"></zn-button>
|
|
322
|
+
</div>
|
|
323
|
+
${this.note ? html`<p class="date-range__note">${this.note}</p>` : nothing}
|
|
324
|
+
<div class="date-range__foot">
|
|
325
|
+
<span class="date-range__summary">
|
|
326
|
+
${from && to ? `${rangeLabel(from, to)} · ${daysBetween(from, to) + 1} days` : 'Pick a last day'}
|
|
327
|
+
</span>
|
|
328
|
+
<zn-button color="transparent" @click="${() => this.close()}">Cancel</zn-button>
|
|
329
|
+
<zn-button class="date-range__apply" color="primary" ?disabled="${!from || !to}"
|
|
330
|
+
@click="${() => this.applyRange(this.draft)}">Apply
|
|
331
|
+
</zn-button>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
</div>`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private renderMonth(year: number, month0: number) {
|
|
338
|
+
const cells = monthGrid(year, month0);
|
|
339
|
+
const rows = Array.from({length: 6}, (_, i) => cells.slice(i * 7, i * 7 + 7)).filter(row => row.some(Boolean));
|
|
340
|
+
const title = new Intl.DateTimeFormat('en-GB', {month: 'long', year: 'numeric', timeZone: 'UTC'})
|
|
341
|
+
.format(new Date(Date.UTC(year, month0, 1)));
|
|
342
|
+
const today = todayIso();
|
|
343
|
+
const {from, to} = this.draft;
|
|
344
|
+
const tabStop = [this.focusDay, from].find(d => d && this.visible(d))
|
|
345
|
+
?? `${this.view.year}-${String(this.view.month0 + 1).padStart(2, '0')}-01`;
|
|
346
|
+
|
|
347
|
+
return html`
|
|
348
|
+
<div class="date-range__month">
|
|
349
|
+
<div class="date-range__head">${title}</div>
|
|
350
|
+
<div class="date-range__grid" role="grid" aria-label="${title}" @keydown="${this.dayKeydown}">
|
|
351
|
+
<div class="date-range__week" role="row">
|
|
352
|
+
${WEEKDAYS.map(d => html`<span role="columnheader">${d}</span>`)}
|
|
353
|
+
</div>
|
|
354
|
+
${rows.map(row => html`
|
|
355
|
+
<div class="date-range__row" role="row">
|
|
356
|
+
${row.map(iso => iso ? html`
|
|
357
|
+
<button type="button" role="gridcell" data-date="${iso}"
|
|
358
|
+
class="${classMap({
|
|
359
|
+
'date-range__day': true,
|
|
360
|
+
'is-start': iso === from,
|
|
361
|
+
'is-end': !!to && iso === to,
|
|
362
|
+
'is-in': !!from && !!to && iso > from && iso < to,
|
|
363
|
+
'is-today': iso === today,
|
|
364
|
+
})}"
|
|
365
|
+
tabindex="${iso === tabStop ? 0 : -1}"
|
|
366
|
+
aria-label="${formatDate(iso)}"
|
|
367
|
+
aria-selected="${!!from && iso >= from && iso <= (to || from)}"
|
|
368
|
+
aria-disabled="${this.isDisabled(iso)}"
|
|
369
|
+
@click="${() => this.choose(iso)}">${Number(iso.slice(8))}</button>` : html`
|
|
370
|
+
<span class="date-range__pad" role="gridcell"></span>`)}
|
|
371
|
+
</div>`)}
|
|
372
|
+
</div>
|
|
373
|
+
</div>`;
|
|
374
|
+
}
|
|
375
|
+
}
|