@adia-ai/web-components 0.6.33 → 0.6.35
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/CHANGELOG.md +64 -0
- package/color/index.js +1 -1
- package/components/accordion/accordion-item.yaml +2 -2
- package/components/accordion/accordion.css +2 -2
- package/components/accordion/accordion.js +1 -1
- package/components/action-list/action-item.yaml +2 -2
- package/components/action-list/action-list.css +2 -2
- package/components/action-list/action-list.js +1 -1
- package/components/agent-artifact/{class.js → agent-artifact.class.js} +1 -1
- package/components/agent-artifact/agent-artifact.css +31 -31
- package/components/agent-artifact/agent-artifact.js +1 -1
- package/components/agent-feedback-bar/agent-feedback-bar.css +10 -10
- package/components/agent-feedback-bar/agent-feedback-bar.js +1 -1
- package/components/agent-questions/agent-questions.css +57 -57
- package/components/agent-questions/agent-questions.js +1 -1
- package/components/agent-reasoning/agent-reasoning.css +62 -62
- package/components/agent-reasoning/agent-reasoning.js +1 -1
- package/components/agent-suggestions/agent-suggestions.css +4 -4
- package/components/agent-suggestions/agent-suggestions.js +1 -1
- package/components/agent-trace/agent-trace.css +53 -53
- package/components/alert/alert.a2ui.json +64 -1
- package/components/alert/{class.js → alert.class.js} +189 -2
- package/components/alert/alert.css +119 -41
- package/components/alert/alert.d.ts +14 -0
- package/components/alert/alert.js +1 -1
- package/components/alert/alert.test.js +184 -0
- package/components/alert/alert.yaml +114 -1
- package/components/avatar/avatar-group.yaml +2 -2
- package/components/avatar/avatar.css +27 -27
- package/components/avatar/avatar.js +1 -1
- package/components/badge/badge.css +27 -27
- package/components/badge/badge.js +1 -1
- package/components/block/block.css +16 -16
- package/components/block/block.js +1 -1
- package/components/breadcrumb/breadcrumb.css +23 -23
- package/components/breadcrumb/breadcrumb.js +1 -1
- package/components/button/button.css +101 -91
- package/components/button/button.js +1 -1
- package/components/calendar-grid/calendar-grid.a2ui.json +146 -0
- package/components/calendar-grid/calendar-grid.class.js +326 -0
- package/components/calendar-grid/calendar-grid.css +246 -0
- package/components/calendar-grid/calendar-grid.d.ts +41 -0
- package/components/calendar-grid/calendar-grid.js +17 -0
- package/components/calendar-grid/calendar-grid.yaml +136 -0
- package/components/calendar-picker/calendar-picker.css +139 -139
- package/components/calendar-picker/calendar-picker.js +1 -1
- package/components/canvas/canvas.css +12 -12
- package/components/card/card.css +83 -83
- package/components/card/card.js +1 -1
- package/components/chart/chart.css +224 -224
- package/components/chart/chart.js +1 -1
- package/components/chart-legend/chart-legend.css +26 -26
- package/components/chart-legend/chart-legend.js +1 -1
- package/components/chat-thread/chat-input.a2ui.json +1 -1
- package/components/chat-thread/chat-input.js +6 -1
- package/components/chat-thread/chat-input.yaml +4 -1
- package/components/chat-thread/chat-thread.js +1 -1
- package/components/check/check.css +40 -40
- package/components/check/check.js +1 -1
- package/components/code/code.css +125 -125
- package/components/code/code.js +1 -1
- package/components/col/col.css +15 -15
- package/components/col/col.js +1 -1
- package/components/color-input/color-input.js +1 -1
- package/components/color-picker/color-picker.css +55 -55
- package/components/color-picker/color-picker.js +1 -1
- package/components/combobox/combobox.a2ui.json +363 -0
- package/components/combobox/combobox.class.js +861 -0
- package/components/combobox/combobox.css +244 -0
- package/components/combobox/combobox.d.ts +113 -0
- package/components/combobox/combobox.examples.md +59 -0
- package/components/combobox/combobox.js +17 -0
- package/components/combobox/combobox.test.js +181 -0
- package/components/combobox/combobox.yaml +369 -0
- package/components/command/command.css +90 -90
- package/components/command/command.js +1 -1
- package/components/date-range-picker/date-range-picker.a2ui.json +300 -0
- package/components/date-range-picker/date-range-picker.class.js +791 -0
- package/components/date-range-picker/date-range-picker.css +224 -0
- package/components/date-range-picker/date-range-picker.d.ts +82 -0
- package/components/date-range-picker/date-range-picker.examples.md +37 -0
- package/components/date-range-picker/date-range-picker.js +17 -0
- package/components/date-range-picker/date-range-picker.test.js +387 -0
- package/components/date-range-picker/date-range-picker.yaml +285 -0
- package/components/datetime-picker/datetime-picker.a2ui.json +334 -0
- package/components/datetime-picker/datetime-picker.class.js +706 -0
- package/components/datetime-picker/datetime-picker.css +150 -0
- package/components/datetime-picker/datetime-picker.d.ts +86 -0
- package/components/datetime-picker/datetime-picker.examples.md +46 -0
- package/components/datetime-picker/datetime-picker.js +17 -0
- package/components/datetime-picker/datetime-picker.test.js +454 -0
- package/components/datetime-picker/datetime-picker.yaml +332 -0
- package/components/demo-toggle/demo-toggle.css +27 -27
- package/components/demo-toggle/demo-toggle.js +1 -1
- package/components/description-list/description-list.css +18 -18
- package/components/description-list/description-list.js +1 -1
- package/components/divider/divider.css +24 -24
- package/components/divider/divider.js +1 -1
- package/components/drawer/drawer.js +1 -1
- package/components/embed/embed.css +6 -6
- package/components/embed/embed.js +1 -1
- package/components/empty-state/empty-state.css +27 -27
- package/components/empty-state/empty-state.js +1 -1
- package/components/feed/feed.css +12 -12
- package/components/feed/feed.js +1 -1
- package/components/field/field.css +28 -28
- package/components/field/field.js +1 -1
- package/components/field/field.test.js +1 -1
- package/components/fields/fields.css +5 -5
- package/components/fields/fields.js +1 -1
- package/components/grid/grid.css +5 -5
- package/components/grid/grid.js +1 -1
- package/components/heatmap/heatmap.css +63 -63
- package/components/heatmap/heatmap.js +1 -1
- package/components/icon/icon.css +12 -12
- package/components/icon/icon.js +1 -1
- package/components/image/image.css +14 -14
- package/components/image/image.js +1 -1
- package/components/index.js +11 -0
- package/components/inline-message/inline-message.a2ui.json +143 -0
- package/components/inline-message/inline-message.class.js +169 -0
- package/components/inline-message/inline-message.css +75 -0
- package/components/inline-message/inline-message.d.ts +31 -0
- package/components/inline-message/inline-message.examples.md +19 -0
- package/components/inline-message/inline-message.js +17 -0
- package/components/inline-message/inline-message.test.js +203 -0
- package/components/inline-message/inline-message.yaml +205 -0
- package/components/input/input.css +67 -67
- package/components/input/input.js +1 -1
- package/components/input/input.yaml +5 -4
- package/components/inspector/inspector.css +6 -6
- package/components/inspector/inspector.js +1 -1
- package/components/integration-card/integration-card.a2ui.json +268 -0
- package/components/integration-card/integration-card.class.js +410 -0
- package/components/integration-card/integration-card.css +169 -0
- package/components/integration-card/integration-card.d.ts +63 -0
- package/components/integration-card/integration-card.examples.md +41 -0
- package/components/integration-card/integration-card.js +17 -0
- package/components/integration-card/integration-card.test.js +306 -0
- package/components/integration-card/integration-card.yaml +280 -0
- package/components/kbd/kbd.css +32 -32
- package/components/kbd/kbd.js +1 -1
- package/components/link/link.css +12 -12
- package/components/link/link.js +1 -1
- package/components/list/list-item.yaml +2 -2
- package/components/list/list.css +8 -8
- package/components/list/list.js +1 -1
- package/components/list-window/list-window.a2ui.json +277 -0
- package/components/list-window/list-window.class.js +688 -0
- package/components/list-window/list-window.css +124 -0
- package/components/list-window/list-window.d.ts +84 -0
- package/components/list-window/list-window.examples.md +73 -0
- package/components/list-window/list-window.js +17 -0
- package/components/list-window/list-window.test.js +303 -0
- package/components/list-window/list-window.yaml +270 -0
- package/components/loading-overlay/loading-overlay.a2ui.json +176 -0
- package/components/loading-overlay/loading-overlay.class.js +203 -0
- package/components/loading-overlay/loading-overlay.css +81 -0
- package/components/loading-overlay/loading-overlay.d.ts +24 -0
- package/components/loading-overlay/loading-overlay.examples.md +50 -0
- package/components/loading-overlay/loading-overlay.js +17 -0
- package/components/loading-overlay/loading-overlay.test.js +257 -0
- package/components/loading-overlay/loading-overlay.yaml +260 -0
- package/components/menu/menu-divider.yaml +1 -1
- package/components/menu/menu-item.yaml +1 -1
- package/components/menu/menu.a2ui.json +3 -0
- package/components/menu/menu.css +8 -8
- package/components/menu/menu.js +1 -1
- package/components/menu/menu.yaml +7 -0
- package/components/modal/{class.js → modal.class.js} +12 -1
- package/components/modal/modal.css +54 -44
- package/components/modal/modal.js +1 -1
- package/components/nav/nav.css +40 -40
- package/components/nav/nav.js +1 -1
- package/components/nav-group/nav-group.css +52 -52
- package/components/nav-group/nav-group.js +1 -1
- package/components/nav-item/nav-item.css +44 -44
- package/components/nav-item/nav-item.js +1 -1
- package/components/noodles/noodles.css +31 -31
- package/components/noodles/noodles.js +1 -1
- package/components/option-card/option-card.css +69 -69
- package/components/option-card/option-card.js +1 -1
- package/components/otp-input/otp-input.css +30 -30
- package/components/otp-input/otp-input.js +1 -1
- package/components/page/page.css +18 -18
- package/components/page/page.js +1 -1
- package/components/pagination/pagination.css +61 -61
- package/components/pagination/pagination.js +1 -1
- package/components/pane/pane.css +57 -57
- package/components/pane/pane.js +1 -1
- package/components/pipeline-status/pipeline-status.css +65 -65
- package/components/pipeline-status/pipeline-status.js +1 -1
- package/components/popover/popover.a2ui.json +8 -1
- package/components/popover/popover.css +17 -17
- package/components/popover/popover.js +1 -1
- package/components/popover/popover.yaml +14 -1
- package/components/progress/progress.css +23 -23
- package/components/progress/progress.js +1 -1
- package/components/progress-row/progress-row.css +17 -17
- package/components/progress-row/progress-row.js +1 -1
- package/components/radio/radio.css +39 -39
- package/components/radio/radio.js +1 -1
- package/components/range/range.css +55 -55
- package/components/range/range.js +1 -1
- package/components/rating/rating.css +28 -28
- package/components/rating/rating.js +1 -1
- package/components/richtext/richtext.css +133 -133
- package/components/richtext/richtext.js +1 -1
- package/components/row/row.css +19 -19
- package/components/row/row.js +1 -1
- package/components/search/search.css +5 -5
- package/components/search/search.js +1 -1
- package/components/segment/segment.css +24 -24
- package/components/segment/segment.js +1 -1
- package/components/segmented/segmented.css +25 -25
- package/components/segmented/segmented.js +1 -1
- package/components/select/select.a2ui.json +58 -4
- package/components/select/{class.js → select.class.js} +415 -6
- package/components/select/select.css +242 -84
- package/components/select/select.d.ts +31 -1
- package/components/select/select.js +1 -1
- package/components/select/select.test.js +202 -0
- package/components/select/select.yaml +126 -5
- package/components/skeleton/skeleton.css +14 -14
- package/components/skeleton/skeleton.js +1 -1
- package/components/slider/slider.css +46 -46
- package/components/slider/slider.js +1 -1
- package/components/spinner/spinner.a2ui.json +198 -0
- package/components/spinner/spinner.class.js +99 -0
- package/components/spinner/spinner.css +221 -0
- package/components/spinner/spinner.d.ts +26 -0
- package/components/spinner/spinner.examples.md +26 -0
- package/components/spinner/spinner.js +17 -0
- package/components/spinner/spinner.test.js +272 -0
- package/components/spinner/spinner.yaml +238 -0
- package/components/stack/stack.css +11 -11
- package/components/stack/stack.js +1 -1
- package/components/stat/stat.css +25 -25
- package/components/step-progress/step-progress.css +20 -20
- package/components/step-progress/step-progress.js +1 -1
- package/components/stepper/stepper-item.yaml +1 -1
- package/components/stepper/stepper.css +29 -29
- package/components/stepper/stepper.js +1 -1
- package/components/stream/stream.css +12 -12
- package/components/stream/stream.js +1 -1
- package/components/swatch/swatch.css +68 -68
- package/components/swatch/swatch.js +1 -1
- package/components/swiper/swiper.css +57 -57
- package/components/swiper/swiper.js +1 -1
- package/components/switch/switch.css +52 -52
- package/components/switch/switch.js +1 -1
- package/components/table/table.css +163 -163
- package/components/table/table.js +1 -1
- package/components/table-toolbar/{class.js → table-toolbar.class.js} +1 -1
- package/components/table-toolbar/table-toolbar.css +32 -32
- package/components/table-toolbar/table-toolbar.js +1 -1
- package/components/tabs/tab.yaml +2 -2
- package/components/tabs/tabs.css +51 -51
- package/components/tabs/tabs.js +1 -1
- package/components/tag/tag.css +48 -48
- package/components/tag/tag.js +1 -1
- package/components/tags-input/tags-input.a2ui.json +337 -0
- package/components/tags-input/tags-input.class.js +776 -0
- package/components/tags-input/tags-input.css +201 -0
- package/components/tags-input/tags-input.d.ts +120 -0
- package/components/tags-input/tags-input.examples.md +92 -0
- package/components/tags-input/tags-input.js +17 -0
- package/components/tags-input/tags-input.test.js +368 -0
- package/components/tags-input/tags-input.yaml +367 -0
- package/components/text/text.css +44 -44
- package/components/text/text.js +1 -1
- package/components/textarea/textarea.a2ui.json +1 -1
- package/components/textarea/textarea.css +46 -46
- package/components/textarea/textarea.js +1 -1
- package/components/textarea/textarea.yaml +11 -8
- package/components/time-picker/time-picker.a2ui.json +267 -0
- package/components/time-picker/time-picker.class.js +693 -0
- package/components/time-picker/time-picker.css +122 -0
- package/components/time-picker/time-picker.d.ts +75 -0
- package/components/time-picker/time-picker.examples.md +35 -0
- package/components/time-picker/time-picker.js +17 -0
- package/components/time-picker/time-picker.test.js +287 -0
- package/components/time-picker/time-picker.yaml +256 -0
- package/components/timeline/timeline-item.yaml +2 -2
- package/components/timeline/{class.js → timeline.class.js} +1 -1
- package/components/timeline/timeline.css +50 -50
- package/components/timeline/timeline.js +1 -1
- package/components/toast/toast.css +58 -58
- package/components/toast/toast.js +1 -1
- package/components/toggle-group/toggle-group.css +6 -6
- package/components/toggle-group/toggle-group.js +1 -1
- package/components/toggle-group/toggle-option.yaml +1 -1
- package/components/toggle-scheme/toggle-scheme.css +2 -2
- package/components/toggle-scheme/toggle-scheme.js +1 -1
- package/components/toolbar/toolbar-group.yaml +1 -1
- package/components/toolbar/toolbar.css +17 -17
- package/components/toolbar/toolbar.js +1 -1
- package/components/tooltip/tooltip.css +2 -2
- package/components/tooltip/tooltip.js +1 -1
- package/components/tree/tree-item.yaml +1 -1
- package/components/tree/tree.css +37 -37
- package/components/tree/tree.js +1 -1
- package/components/upload/upload.css +49 -49
- package/components/upload/upload.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +146 -87
- package/package.json +3 -3
- package/styles/components.css +11 -0
- /package/components/accordion/{class.js → accordion.class.js} +0 -0
- /package/components/action-list/{class.js → action-list.class.js} +0 -0
- /package/components/agent-feedback-bar/{class.js → agent-feedback-bar.class.js} +0 -0
- /package/components/agent-questions/{class.js → agent-questions.class.js} +0 -0
- /package/components/agent-reasoning/{class.js → agent-reasoning.class.js} +0 -0
- /package/components/agent-suggestions/{class.js → agent-suggestions.class.js} +0 -0
- /package/components/avatar/{class.js → avatar.class.js} +0 -0
- /package/components/badge/{class.js → badge.class.js} +0 -0
- /package/components/block/{class.js → block.class.js} +0 -0
- /package/components/breadcrumb/{class.js → breadcrumb.class.js} +0 -0
- /package/components/button/{class.js → button.class.js} +0 -0
- /package/components/calendar-picker/{class.js → calendar-picker.class.js} +0 -0
- /package/components/card/{class.js → card.class.js} +0 -0
- /package/components/chart/{class.js → chart.class.js} +0 -0
- /package/components/chart-legend/{class.js → chart-legend.class.js} +0 -0
- /package/components/chat-thread/{class.js → chat-thread.class.js} +0 -0
- /package/components/check/{class.js → check.class.js} +0 -0
- /package/components/code/{class.js → code.class.js} +0 -0
- /package/components/col/{class.js → col.class.js} +0 -0
- /package/components/color-input/{class.js → color-input.class.js} +0 -0
- /package/components/color-picker/{class.js → color-picker.class.js} +0 -0
- /package/components/command/{class.js → command.class.js} +0 -0
- /package/components/demo-toggle/{class.js → demo-toggle.class.js} +0 -0
- /package/components/description-list/{class.js → description-list.class.js} +0 -0
- /package/components/divider/{class.js → divider.class.js} +0 -0
- /package/components/drawer/{class.js → drawer.class.js} +0 -0
- /package/components/embed/{class.js → embed.class.js} +0 -0
- /package/components/empty-state/{class.js → empty-state.class.js} +0 -0
- /package/components/feed/{class.js → feed.class.js} +0 -0
- /package/components/field/{class.js → field.class.js} +0 -0
- /package/components/fields/{class.js → fields.class.js} +0 -0
- /package/components/grid/{class.js → grid.class.js} +0 -0
- /package/components/heatmap/{class.js → heatmap.class.js} +0 -0
- /package/components/icon/{class.js → icon.class.js} +0 -0
- /package/components/image/{class.js → image.class.js} +0 -0
- /package/components/input/{class.js → input.class.js} +0 -0
- /package/components/inspector/{class.js → inspector.class.js} +0 -0
- /package/components/kbd/{class.js → kbd.class.js} +0 -0
- /package/components/link/{class.js → link.class.js} +0 -0
- /package/components/list/{class.js → list.class.js} +0 -0
- /package/components/menu/{class.js → menu.class.js} +0 -0
- /package/components/nav/{class.js → nav.class.js} +0 -0
- /package/components/nav-group/{class.js → nav-group.class.js} +0 -0
- /package/components/nav-item/{class.js → nav-item.class.js} +0 -0
- /package/components/noodles/{class.js → noodles.class.js} +0 -0
- /package/components/option-card/{class.js → option-card.class.js} +0 -0
- /package/components/otp-input/{class.js → otp-input.class.js} +0 -0
- /package/components/page/{class.js → page.class.js} +0 -0
- /package/components/pagination/{class.js → pagination.class.js} +0 -0
- /package/components/pane/{class.js → pane.class.js} +0 -0
- /package/components/pipeline-status/{class.js → pipeline-status.class.js} +0 -0
- /package/components/popover/{class.js → popover.class.js} +0 -0
- /package/components/progress/{class.js → progress.class.js} +0 -0
- /package/components/progress-row/{class.js → progress-row.class.js} +0 -0
- /package/components/radio/{class.js → radio.class.js} +0 -0
- /package/components/range/{class.js → range.class.js} +0 -0
- /package/components/rating/{class.js → rating.class.js} +0 -0
- /package/components/richtext/{class.js → richtext.class.js} +0 -0
- /package/components/row/{class.js → row.class.js} +0 -0
- /package/components/search/{class.js → search.class.js} +0 -0
- /package/components/segment/{class.js → segment.class.js} +0 -0
- /package/components/segmented/{class.js → segmented.class.js} +0 -0
- /package/components/skeleton/{class.js → skeleton.class.js} +0 -0
- /package/components/slider/{class.js → slider.class.js} +0 -0
- /package/components/stack/{class.js → stack.class.js} +0 -0
- /package/components/step-progress/{class.js → step-progress.class.js} +0 -0
- /package/components/stepper/{class.js → stepper.class.js} +0 -0
- /package/components/stream/{class.js → stream.class.js} +0 -0
- /package/components/swatch/{class.js → swatch.class.js} +0 -0
- /package/components/swiper/{class.js → swiper.class.js} +0 -0
- /package/components/switch/{class.js → switch.class.js} +0 -0
- /package/components/table/{class.js → table.class.js} +0 -0
- /package/components/tabs/{class.js → tabs.class.js} +0 -0
- /package/components/tag/{class.js → tag.class.js} +0 -0
- /package/components/text/{class.js → text.class.js} +0 -0
- /package/components/textarea/{class.js → textarea.class.js} +0 -0
- /package/components/toast/{class.js → toast.class.js} +0 -0
- /package/components/toggle-group/{class.js → toggle-group.class.js} +0 -0
- /package/components/toggle-scheme/{class.js → toggle-scheme.class.js} +0 -0
- /package/components/toolbar/{class.js → toolbar.class.js} +0 -0
- /package/components/tooltip/{class.js → tooltip.class.js} +0 -0
- /package/components/tree/{class.js → tree.class.js} +0 -0
- /package/components/upload/{class.js → upload.class.js} +0 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-side-effect class export for `<date-range-picker-ui>`.
|
|
3
|
+
*
|
|
4
|
+
* Importing this file gives you the class without auto-registering the tag.
|
|
5
|
+
* Useful for test isolation, subclassing with tag-name override, or selective
|
|
6
|
+
* composition.
|
|
7
|
+
*
|
|
8
|
+
* The auto-register path stays at `@adia-ai/web-components/components/date-range-picker`
|
|
9
|
+
* (which imports this file + calls `defineIfFree()`).
|
|
10
|
+
*
|
|
11
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* <date-range-picker-ui>
|
|
16
|
+
*
|
|
17
|
+
* Compound form primitive for selecting a start + end date pair with
|
|
18
|
+
* optional preset shortcuts. Composes <calendar-picker-ui> (two
|
|
19
|
+
* instances — start + end pane) + <popover-ui> + <button-ui> for the
|
|
20
|
+
* trigger and preset rail.
|
|
21
|
+
*
|
|
22
|
+
* Form participation:
|
|
23
|
+
* ElementInternals serializes the selected range as JSON:
|
|
24
|
+
* `{"from":"2026-01-01","to":"2026-01-07"}`
|
|
25
|
+
* under the [name] attribute. With [comparison] set, a secondary
|
|
26
|
+
* field `<name>-compare` carries the comparison range.
|
|
27
|
+
*
|
|
28
|
+
* Keyboard:
|
|
29
|
+
* Enter/Space (trigger) — open popover, focus start pane
|
|
30
|
+
* Escape (open) — close popover (no commit)
|
|
31
|
+
* Tab (open) — cycle trigger → start cal → end cal → presets → footer
|
|
32
|
+
* Arrows (calendar) — navigate days (delegated to <calendar-picker-ui>)
|
|
33
|
+
*
|
|
34
|
+
* A11y:
|
|
35
|
+
* role=combobox on the host; role=dialog on the popover; focus moves
|
|
36
|
+
* to the start calendar's grid on open and returns to the trigger on
|
|
37
|
+
* close. Tab is trapped inside the popover while open.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { UIFormElement } from '../../core/form.js';
|
|
41
|
+
import { anchorPopover } from '../../core/anchor.js';
|
|
42
|
+
import { untracked } from '../../core/signals.js';
|
|
43
|
+
|
|
44
|
+
const MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
45
|
+
const MONTHS_LONG = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
46
|
+
|
|
47
|
+
function pad(n) { return String(n).padStart(2, '0'); }
|
|
48
|
+
|
|
49
|
+
function toISO(d) {
|
|
50
|
+
if (!d) return '';
|
|
51
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseISO(str) {
|
|
55
|
+
if (!str) return null;
|
|
56
|
+
const m = String(str).match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
57
|
+
if (!m) return null;
|
|
58
|
+
const d = new Date(+m[1], +m[2] - 1, +m[3]);
|
|
59
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseRange(v) {
|
|
63
|
+
if (v == null) return null;
|
|
64
|
+
if (typeof v === 'object') {
|
|
65
|
+
if (!v.from || !v.to) return null;
|
|
66
|
+
return { from: String(v.from), to: String(v.to) };
|
|
67
|
+
}
|
|
68
|
+
if (typeof v !== 'string' || !v.trim()) return null;
|
|
69
|
+
try {
|
|
70
|
+
const obj = JSON.parse(v);
|
|
71
|
+
if (obj && obj.from && obj.to) return { from: String(obj.from), to: String(obj.to) };
|
|
72
|
+
} catch {}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function rangeIsOrdered(r) {
|
|
77
|
+
if (!r) return false;
|
|
78
|
+
return r.from <= r.to;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatDate(iso, fmt) {
|
|
82
|
+
const d = parseISO(iso);
|
|
83
|
+
if (!d) return '';
|
|
84
|
+
switch (fmt) {
|
|
85
|
+
case 'long':
|
|
86
|
+
return `${MONTHS_LONG[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
|
|
87
|
+
case 'iso':
|
|
88
|
+
return iso;
|
|
89
|
+
case 'short':
|
|
90
|
+
default:
|
|
91
|
+
return `${MONTHS_SHORT[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Default presets (today is computed lazily on first render). */
|
|
96
|
+
function defaultPresets(today = new Date()) {
|
|
97
|
+
const t0 = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
98
|
+
const isoToday = toISO(t0);
|
|
99
|
+
const sub = (days) => {
|
|
100
|
+
const d = new Date(t0);
|
|
101
|
+
d.setDate(d.getDate() - days);
|
|
102
|
+
return toISO(d);
|
|
103
|
+
};
|
|
104
|
+
const startOfMonth = toISO(new Date(t0.getFullYear(), t0.getMonth(), 1));
|
|
105
|
+
const endOfMonth = toISO(new Date(t0.getFullYear(), t0.getMonth() + 1, 0));
|
|
106
|
+
const startOfYear = toISO(new Date(t0.getFullYear(), 0, 1));
|
|
107
|
+
return [
|
|
108
|
+
{ label: 'Today', range: { from: isoToday, to: isoToday } },
|
|
109
|
+
{ label: 'Last 7 days', range: { from: sub(6), to: isoToday } },
|
|
110
|
+
{ label: 'Last 30 days', range: { from: sub(29), to: isoToday } },
|
|
111
|
+
{ label: 'This month', range: { from: startOfMonth, to: endOfMonth } },
|
|
112
|
+
{ label: 'Year to date', range: { from: startOfYear, to: isoToday } },
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Default comparison presets (analytics surfaces). */
|
|
117
|
+
function defaultComparisonPresets() {
|
|
118
|
+
return [
|
|
119
|
+
{ label: 'Previous period' },
|
|
120
|
+
{ label: 'Same period last year' },
|
|
121
|
+
{ label: 'Same period last month' },
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class UIDateRangePicker extends UIFormElement {
|
|
126
|
+
// Per AGENTS.md §170: label is NOT first-class here — use <field-ui label>.
|
|
127
|
+
static labelDeprecated = true;
|
|
128
|
+
|
|
129
|
+
// §154: Phosphor icons this primitive auto-stamps (without consumer
|
|
130
|
+
// markup). Aggregated by installIconLoadersForRegistered() across all
|
|
131
|
+
// defined elements. Audited by check-required-icons.mjs.
|
|
132
|
+
static requiredIcons = ['calendar', 'caret-down', 'arrow-right'];
|
|
133
|
+
|
|
134
|
+
static get properties() {
|
|
135
|
+
return {
|
|
136
|
+
...UIFormElement.properties,
|
|
137
|
+
// value/compareValue are STRINGS at the attribute boundary (JSON
|
|
138
|
+
// serialized); typed accessors expose the parsed object form.
|
|
139
|
+
compareValue: { type: String, default: '', attribute: 'compare-value' },
|
|
140
|
+
comparison: { type: Boolean, default: false, reflect: true },
|
|
141
|
+
min: { type: String, default: '', reflect: true },
|
|
142
|
+
max: { type: String, default: '', reflect: true },
|
|
143
|
+
open: { type: Boolean, default: false, reflect: true },
|
|
144
|
+
placeholder: { type: String, default: 'Select range', reflect: true },
|
|
145
|
+
format: { type: String, default: 'short', reflect: true },
|
|
146
|
+
noPresets: { type: Boolean, default: false, attribute: 'no-presets', reflect: true },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Static parts — fixed structural skeleton stamped on first connect.
|
|
151
|
+
// The popover is a `popover="manual"` element so we control showPopover()
|
|
152
|
+
// / hidePopover() programmatically. Two calendar instances + a preset
|
|
153
|
+
// rail + a calendar area sit inside.
|
|
154
|
+
static parts = {
|
|
155
|
+
trigger: '<button-ui slot="trigger" variant="outline" type="button" icon="calendar" trailing-icon="caret-down" aria-label="Open date range picker"></button-ui>',
|
|
156
|
+
popover: '<div slot="popover" popover="manual" role="dialog" aria-label="Date range picker"></div>',
|
|
157
|
+
presets: '<div data-preset-rail role="group" aria-label="Date range presets"></div>',
|
|
158
|
+
calArea: '<div data-calendar-area></div>',
|
|
159
|
+
// Substrate primitive — bare grid, no trigger / no popover (added §FB-Wave1-QA).
|
|
160
|
+
calFrom: '<calendar-grid-ui data-cal-from aria-label="Start date"></calendar-grid-ui>',
|
|
161
|
+
calTo: '<calendar-grid-ui data-cal-to aria-label="End date"></calendar-grid-ui>',
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// No template — date-range-picker composes from authored light-DOM
|
|
165
|
+
// children + ensured parts. An empty html`` result would trigger
|
|
166
|
+
// stamp() → replaceChildren(), wiping authored [slot="presets|footer"]
|
|
167
|
+
// before render() can migrate them.
|
|
168
|
+
static template = () => null;
|
|
169
|
+
|
|
170
|
+
// ── State ─────────────────────────────────────────────────────────
|
|
171
|
+
#presetsAttr = null; // explicit override via property
|
|
172
|
+
#pending = null; // partial range during selection
|
|
173
|
+
#previousFocus = null; // focus restoration target on close
|
|
174
|
+
#bound = false;
|
|
175
|
+
#popoverShown = false; // local mirror of popover open state
|
|
176
|
+
#triggerRef = null;
|
|
177
|
+
#popoverRef = null;
|
|
178
|
+
#presetRailRef = null;
|
|
179
|
+
#calFromRef = null;
|
|
180
|
+
#calToRef = null;
|
|
181
|
+
#anchorCleanup = null; // §FB-Wave1-QA — popover anchor positioning cleanup
|
|
182
|
+
|
|
183
|
+
// ── Public accessors ──────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/** Parsed `{from, to}` object form of the value attribute (or null). */
|
|
186
|
+
get rangeValue() {
|
|
187
|
+
return parseRange(this.value);
|
|
188
|
+
}
|
|
189
|
+
set rangeValue(v) {
|
|
190
|
+
if (v == null) {
|
|
191
|
+
this.value = '';
|
|
192
|
+
} else {
|
|
193
|
+
this.value = JSON.stringify({ from: String(v.from), to: String(v.to) });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Parsed `{from, to}` object form of compare-value (or null). */
|
|
198
|
+
get rangeCompareValue() {
|
|
199
|
+
return parseRange(this.compareValue);
|
|
200
|
+
}
|
|
201
|
+
set rangeCompareValue(v) {
|
|
202
|
+
if (v == null) {
|
|
203
|
+
this.compareValue = '';
|
|
204
|
+
} else {
|
|
205
|
+
this.compareValue = JSON.stringify({ from: String(v.from), to: String(v.to) });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Override the preset list. Setter accepts an array of `{label, range}`. */
|
|
210
|
+
get presets() {
|
|
211
|
+
if (Array.isArray(this.#presetsAttr)) return this.#presetsAttr;
|
|
212
|
+
return defaultPresets();
|
|
213
|
+
}
|
|
214
|
+
set presets(arr) {
|
|
215
|
+
// Wrap in untracked() so reactive-property reads in this setter don't
|
|
216
|
+
// leak subscriptions upward (per MEMORY entry: custom setter `untracked` wrap).
|
|
217
|
+
untracked(() => {
|
|
218
|
+
this.#presetsAttr = Array.isArray(arr) ? arr.slice() : null;
|
|
219
|
+
if (this.isConnected) this.#renderPresets();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Imperative API ────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
open$() { this.open = true; }
|
|
226
|
+
close$() { this.open = false; }
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Programmatically open the popover (matches the spec §4 method shape
|
|
230
|
+
* `open()`). The reactive prop `open` is already exposed via the
|
|
231
|
+
* setter; this method is the public commit point that also wires
|
|
232
|
+
* focus + dispatches `open` event with trigger='programmatic'.
|
|
233
|
+
*/
|
|
234
|
+
openPopover() {
|
|
235
|
+
if (this.disabled || this.readonly) return;
|
|
236
|
+
if (!this.open) {
|
|
237
|
+
this.#previousFocus = document.activeElement;
|
|
238
|
+
this.open = true;
|
|
239
|
+
this.dispatchEvent(new CustomEvent('open', { bubbles: true, detail: { trigger: 'programmatic' } }));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Programmatically close. */
|
|
244
|
+
closePopover(reason = 'programmatic') {
|
|
245
|
+
if (!this.open) return;
|
|
246
|
+
this.open = false;
|
|
247
|
+
this.dispatchEvent(new CustomEvent('close', { bubbles: true, detail: { reason } }));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Reset value (and compareValue when comparison is set). */
|
|
251
|
+
clear() {
|
|
252
|
+
this.value = '';
|
|
253
|
+
if (this.comparison) this.compareValue = '';
|
|
254
|
+
this.#pending = null;
|
|
255
|
+
this.syncValue('');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Look up a preset by label and apply it. Returns true on hit. */
|
|
259
|
+
applyPreset(label) {
|
|
260
|
+
const preset = this.presets.find((p) => p.label === label);
|
|
261
|
+
if (!preset || !preset.range) return false;
|
|
262
|
+
this.#commitRange(preset.range, { presetLabel: preset.label });
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Constraint validation ─────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Override `syncValue` so the form value reflects the JSON-serialized
|
|
270
|
+
* range. The base UIFormElement::syncValue writes raw `value` to the
|
|
271
|
+
* form; we additionally write the comparison field if applicable.
|
|
272
|
+
*/
|
|
273
|
+
syncValue(val) {
|
|
274
|
+
const baseVal = val ?? this.value ?? '';
|
|
275
|
+
const range = parseRange(baseVal);
|
|
276
|
+
if (this.comparison && this.name) {
|
|
277
|
+
// Compose a FormData so two named fields land in the form.
|
|
278
|
+
const fd = new FormData();
|
|
279
|
+
if (range) fd.append(this.name, baseVal);
|
|
280
|
+
const cmp = parseRange(this.compareValue);
|
|
281
|
+
if (cmp) fd.append(`${this.name}-compare`, this.compareValue);
|
|
282
|
+
this.internals.setFormValue(fd);
|
|
283
|
+
} else {
|
|
284
|
+
this.internals.setFormValue(range ? baseVal : '');
|
|
285
|
+
}
|
|
286
|
+
this.#runRangeConstraints(range);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#runRangeConstraints(range) {
|
|
290
|
+
if (this.required && !range) {
|
|
291
|
+
this.internals.setValidity(
|
|
292
|
+
{ valueMissing: true },
|
|
293
|
+
this.getAttribute('data-msg-required') || 'Please select a date range.',
|
|
294
|
+
this,
|
|
295
|
+
);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
if (range && !rangeIsOrdered(range)) {
|
|
299
|
+
this.internals.setValidity(
|
|
300
|
+
{ rangeUnderflow: true },
|
|
301
|
+
'End date must be on or after start date.',
|
|
302
|
+
this,
|
|
303
|
+
);
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (range && this.min && range.from < this.min) {
|
|
307
|
+
this.internals.setValidity(
|
|
308
|
+
{ rangeUnderflow: true },
|
|
309
|
+
`Earliest selectable date is ${this.min}.`,
|
|
310
|
+
this,
|
|
311
|
+
);
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
if (range && this.max && range.to > this.max) {
|
|
315
|
+
this.internals.setValidity(
|
|
316
|
+
{ rangeOverflow: true },
|
|
317
|
+
`Latest selectable date is ${this.max}.`,
|
|
318
|
+
this,
|
|
319
|
+
);
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
this.internals.setValidity({});
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── Lifecycle ─────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
connected() {
|
|
329
|
+
super.connected();
|
|
330
|
+
this.setAttribute('role', 'combobox');
|
|
331
|
+
this.setAttribute('aria-haspopup', 'dialog');
|
|
332
|
+
this.setAttribute('aria-expanded', this.open ? 'true' : 'false');
|
|
333
|
+
|
|
334
|
+
// Stamp parts via this.ensure() — idempotent. Wire listeners once.
|
|
335
|
+
if (!this.#bound) {
|
|
336
|
+
this.#bound = true;
|
|
337
|
+
this.#ensureParts();
|
|
338
|
+
this.#triggerRef.addEventListener('click', this.#onTriggerClick);
|
|
339
|
+
// Register keydown in CAPTURE phase so we set the keyboard-origin
|
|
340
|
+
// flag BEFORE <button-ui>'s own keydown handler synthesizes a
|
|
341
|
+
// click via `this.click()` (see button/button.class.js:148).
|
|
342
|
+
this.#triggerRef.addEventListener('keydown', this.#onTriggerKey, true);
|
|
343
|
+
this.#popoverRef.addEventListener('click', this.#onPopoverClick);
|
|
344
|
+
this.#popoverRef.addEventListener('keydown', this.#onPopoverKey);
|
|
345
|
+
this.#calFromRef.addEventListener('change', this.#onCalFromChange);
|
|
346
|
+
this.#calToRef.addEventListener('change', this.#onCalToChange);
|
|
347
|
+
}
|
|
348
|
+
// Wire form value on first connect.
|
|
349
|
+
this.syncValue();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
disconnected() {
|
|
353
|
+
super.disconnected();
|
|
354
|
+
if (this.#triggerRef) {
|
|
355
|
+
this.#triggerRef.removeEventListener('click', this.#onTriggerClick);
|
|
356
|
+
// Capture-phase removal must mirror the capture-phase registration.
|
|
357
|
+
this.#triggerRef.removeEventListener('keydown', this.#onTriggerKey, true);
|
|
358
|
+
}
|
|
359
|
+
if (this.#popoverRef) {
|
|
360
|
+
this.#popoverRef.removeEventListener('click', this.#onPopoverClick);
|
|
361
|
+
this.#popoverRef.removeEventListener('keydown', this.#onPopoverKey);
|
|
362
|
+
this.#popoverRef.hidePopover?.();
|
|
363
|
+
}
|
|
364
|
+
if (this.#calFromRef) this.#calFromRef.removeEventListener('change', this.#onCalFromChange);
|
|
365
|
+
if (this.#calToRef) this.#calToRef.removeEventListener('change', this.#onCalToChange);
|
|
366
|
+
document.removeEventListener('pointerdown', this.#onOutside);
|
|
367
|
+
document.removeEventListener('keydown', this.#onDocKey);
|
|
368
|
+
this.#anchorCleanup?.();
|
|
369
|
+
this.#anchorCleanup = null;
|
|
370
|
+
this.#bound = false;
|
|
371
|
+
this.#popoverShown = false;
|
|
372
|
+
this.#triggerRef = null;
|
|
373
|
+
this.#popoverRef = null;
|
|
374
|
+
this.#presetRailRef = null;
|
|
375
|
+
this.#calFromRef = null;
|
|
376
|
+
this.#calToRef = null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#ensureParts() {
|
|
380
|
+
// Trigger — light DOM. Default <button-ui> stamped unless author
|
|
381
|
+
// supplied [slot="trigger"].
|
|
382
|
+
this.#triggerRef = this.ensure('trigger');
|
|
383
|
+
// Popover is a div hosted in light DOM but lifted to the top layer
|
|
384
|
+
// via the Popover API on showPopover().
|
|
385
|
+
this.#popoverRef = this.ensure('popover');
|
|
386
|
+
if (this.id) this.#popoverRef.setAttribute('aria-labelledby', this.id);
|
|
387
|
+
// Calendar area lives inside the popover.
|
|
388
|
+
if (!this.#popoverRef.querySelector(':scope > [data-calendar-area]')) {
|
|
389
|
+
const calArea = this.constructor._pp.calArea.cloneNode(true);
|
|
390
|
+
this.#popoverRef.appendChild(calArea);
|
|
391
|
+
}
|
|
392
|
+
// Preset rail (unless author supplied [slot="presets"] OR no-presets is set).
|
|
393
|
+
const authorPresets = this.querySelector(':scope > [slot="presets"]');
|
|
394
|
+
if (authorPresets && authorPresets.parentElement !== this.#popoverRef) {
|
|
395
|
+
this.#popoverRef.appendChild(authorPresets);
|
|
396
|
+
this.#presetRailRef = authorPresets;
|
|
397
|
+
} else if (!this.noPresets && !authorPresets) {
|
|
398
|
+
if (!this.#popoverRef.querySelector(':scope > [data-preset-rail]')) {
|
|
399
|
+
const rail = this.constructor._pp.presets.cloneNode(true);
|
|
400
|
+
this.#popoverRef.insertBefore(rail, this.#popoverRef.querySelector(':scope > [data-calendar-area]'));
|
|
401
|
+
}
|
|
402
|
+
this.#presetRailRef = this.#popoverRef.querySelector(':scope > [data-preset-rail]');
|
|
403
|
+
}
|
|
404
|
+
// Two calendar panes — start + end.
|
|
405
|
+
const calArea = this.#popoverRef.querySelector(':scope > [data-calendar-area]');
|
|
406
|
+
if (!calArea.querySelector(':scope > [data-cal-from]')) {
|
|
407
|
+
const calFrom = this.constructor._pp.calFrom.cloneNode(true);
|
|
408
|
+
calArea.appendChild(calFrom);
|
|
409
|
+
}
|
|
410
|
+
if (!calArea.querySelector(':scope > [data-cal-to]')) {
|
|
411
|
+
const calTo = this.constructor._pp.calTo.cloneNode(true);
|
|
412
|
+
calArea.appendChild(calTo);
|
|
413
|
+
}
|
|
414
|
+
this.#calFromRef = calArea.querySelector(':scope > [data-cal-from]');
|
|
415
|
+
this.#calToRef = calArea.querySelector(':scope > [data-cal-to]');
|
|
416
|
+
// Footer slot — author-supplied; lift into popover after calendar area.
|
|
417
|
+
const authorFooter = this.querySelector(':scope > [slot="footer"]');
|
|
418
|
+
if (authorFooter && authorFooter.parentElement !== this.#popoverRef) {
|
|
419
|
+
this.#popoverRef.appendChild(authorFooter);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
render() {
|
|
424
|
+
// Mirror reactive props onto the part nodes — runs in the element's
|
|
425
|
+
// effect every time a tracked prop changes.
|
|
426
|
+
if (!this.#triggerRef) return;
|
|
427
|
+
this.#triggerRef.setAttribute('text', this.#displayText());
|
|
428
|
+
this.#triggerRef.toggleAttribute('disabled', this.disabled);
|
|
429
|
+
this.#triggerRef.setAttribute('aria-haspopup', 'dialog');
|
|
430
|
+
this.#triggerRef.setAttribute('aria-expanded', this.open ? 'true' : 'false');
|
|
431
|
+
this.setAttribute('aria-expanded', this.open ? 'true' : 'false');
|
|
432
|
+
|
|
433
|
+
// Cascade min/max + selection state into the calendar panes.
|
|
434
|
+
const range = parseRange(this.value);
|
|
435
|
+
const pending = this.#pending;
|
|
436
|
+
// Effective endpoints — pending (mid-click) takes precedence over
|
|
437
|
+
// value. Push the SAME from/to onto BOTH grids so each independently
|
|
438
|
+
// lights up its in-range cells via `[data-in-range]`. Without this,
|
|
439
|
+
// only the two endpoint cells got `[data-selected]` and the days
|
|
440
|
+
// between them rendered as plain background — the "fill the cells
|
|
441
|
+
// between start and end dates" trap.
|
|
442
|
+
const effFrom = pending?.from || range?.from || '';
|
|
443
|
+
const effTo = pending?.to || range?.to || '';
|
|
444
|
+
const setRangeAttrs = (cal) => {
|
|
445
|
+
if (effFrom) cal.setAttribute('range-start', effFrom);
|
|
446
|
+
else cal.removeAttribute('range-start');
|
|
447
|
+
if (effTo) cal.setAttribute('range-end', effTo);
|
|
448
|
+
else cal.removeAttribute('range-end');
|
|
449
|
+
};
|
|
450
|
+
if (this.#calFromRef) {
|
|
451
|
+
if (this.min) this.#calFromRef.setAttribute('min', this.min);
|
|
452
|
+
if (this.max) this.#calFromRef.setAttribute('max', this.max);
|
|
453
|
+
this.#calFromRef.value = pending?.from || range?.from || '';
|
|
454
|
+
setRangeAttrs(this.#calFromRef);
|
|
455
|
+
}
|
|
456
|
+
if (this.#calToRef) {
|
|
457
|
+
if (this.min) this.#calToRef.setAttribute('min', this.min);
|
|
458
|
+
if (this.max) this.#calToRef.setAttribute('max', this.max);
|
|
459
|
+
this.#calToRef.value = pending?.to || range?.to || '';
|
|
460
|
+
setRangeAttrs(this.#calToRef);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Render the preset rail (unless overridden by [slot="presets"] or
|
|
464
|
+
// hidden via [no-presets]).
|
|
465
|
+
if (!this.querySelector(':scope > [slot="presets"]')) {
|
|
466
|
+
if (this.noPresets) {
|
|
467
|
+
this.#presetRailRef?.remove();
|
|
468
|
+
this.#presetRailRef = null;
|
|
469
|
+
} else {
|
|
470
|
+
if (!this.#presetRailRef) this.#ensureParts();
|
|
471
|
+
this.#renderPresets();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Toggle the popover via the Popover API. We track open state
|
|
476
|
+
// locally so the toggle works under happy-dom (where `:popover-open`
|
|
477
|
+
// selector matching is unreliable) and in real browsers.
|
|
478
|
+
if (this.#popoverRef) {
|
|
479
|
+
if (this.open && !this.#popoverShown) {
|
|
480
|
+
if (!this.disabled && !this.readonly) {
|
|
481
|
+
this.#popoverShown = true;
|
|
482
|
+
this.#popoverRef.showPopover?.();
|
|
483
|
+
// §FB-Wave1-QA — anchor the popover to the trigger via the canonical
|
|
484
|
+
// helper. Without this, the popover renders at viewport (0,0).
|
|
485
|
+
// Matches select-ui + calendar-picker-ui's anchorPopover pattern.
|
|
486
|
+
this.#anchorCleanup?.();
|
|
487
|
+
this.#anchorCleanup = anchorPopover(this.#triggerRef, this.#popoverRef, {
|
|
488
|
+
placement: this.getAttribute('placement') || 'bottom-start',
|
|
489
|
+
gap: 4,
|
|
490
|
+
});
|
|
491
|
+
document.addEventListener('pointerdown', this.#onOutside);
|
|
492
|
+
document.addEventListener('keydown', this.#onDocKey);
|
|
493
|
+
// Defer focus to next microtask so the grid is paint-stable.
|
|
494
|
+
queueMicrotask(() => {
|
|
495
|
+
if (!this.open) return;
|
|
496
|
+
const grid = this.#calFromRef?.querySelector?.('[data-cal-day]:not([disabled]):not([data-outside])');
|
|
497
|
+
grid?.focus?.();
|
|
498
|
+
});
|
|
499
|
+
} else {
|
|
500
|
+
// Disabled or readonly — refuse to open.
|
|
501
|
+
this.open = false;
|
|
502
|
+
}
|
|
503
|
+
} else if (!this.open && this.#popoverShown) {
|
|
504
|
+
this.#popoverShown = false;
|
|
505
|
+
this.#anchorCleanup?.();
|
|
506
|
+
this.#anchorCleanup = null;
|
|
507
|
+
this.#popoverRef.hidePopover?.();
|
|
508
|
+
document.removeEventListener('pointerdown', this.#onOutside);
|
|
509
|
+
document.removeEventListener('keydown', this.#onDocKey);
|
|
510
|
+
// Return focus to the trigger.
|
|
511
|
+
this.#triggerRef?.focus?.();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── Trigger text formatter ────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
#displayText() {
|
|
519
|
+
const range = parseRange(this.value);
|
|
520
|
+
if (!range) return this.placeholder;
|
|
521
|
+
const from = formatDate(range.from, this.format);
|
|
522
|
+
const to = formatDate(range.to, this.format);
|
|
523
|
+
if (!from || !to) return this.placeholder;
|
|
524
|
+
return `${from} → ${to}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ── Preset rail rendering ─────────────────────────────────────────
|
|
528
|
+
|
|
529
|
+
#renderPresets() {
|
|
530
|
+
if (!this.#presetRailRef) return;
|
|
531
|
+
// Wipe + re-stamp; preset cardinality is small (≤10).
|
|
532
|
+
this.#presetRailRef.replaceChildren();
|
|
533
|
+
for (const preset of this.presets) {
|
|
534
|
+
const btn = document.createElement('button-ui');
|
|
535
|
+
btn.setAttribute('variant', 'ghost');
|
|
536
|
+
btn.setAttribute('type', 'button');
|
|
537
|
+
btn.setAttribute('text', preset.label);
|
|
538
|
+
btn.setAttribute('size', 'sm');
|
|
539
|
+
btn.dataset.presetLabel = preset.label;
|
|
540
|
+
this.#presetRailRef.appendChild(btn);
|
|
541
|
+
}
|
|
542
|
+
// Comparison cluster (when [comparison] is set).
|
|
543
|
+
if (this.comparison) {
|
|
544
|
+
const divider = document.createElement('divider-ui');
|
|
545
|
+
divider.dataset.presetDivider = '';
|
|
546
|
+
this.#presetRailRef.appendChild(divider);
|
|
547
|
+
for (const preset of defaultComparisonPresets()) {
|
|
548
|
+
const btn = document.createElement('button-ui');
|
|
549
|
+
btn.setAttribute('variant', 'ghost');
|
|
550
|
+
btn.setAttribute('type', 'button');
|
|
551
|
+
btn.setAttribute('text', preset.label);
|
|
552
|
+
btn.setAttribute('size', 'sm');
|
|
553
|
+
btn.dataset.comparisonPresetLabel = preset.label;
|
|
554
|
+
this.#presetRailRef.appendChild(btn);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ── Event handlers ────────────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
// Flag set by #onTriggerKey to mark the *next* click as keyboard-originated.
|
|
562
|
+
// <button-ui> calls `this.click()` from its own Enter/Space keydown handler
|
|
563
|
+
// (see button/button.class.js:148), so the click that follows a keyboard press is
|
|
564
|
+
// synthetic. We use this flag to disambiguate the `trigger:` field on the
|
|
565
|
+
// `open` event.
|
|
566
|
+
#keyboardOpen = false;
|
|
567
|
+
|
|
568
|
+
#onTriggerClick = (e) => {
|
|
569
|
+
if (this.disabled) return;
|
|
570
|
+
if (this.readonly) return;
|
|
571
|
+
e.stopPropagation();
|
|
572
|
+
const fromKeyboard = this.#keyboardOpen;
|
|
573
|
+
this.#keyboardOpen = false;
|
|
574
|
+
if (this.open) {
|
|
575
|
+
this.closePopover('outside');
|
|
576
|
+
} else {
|
|
577
|
+
this.#previousFocus = document.activeElement;
|
|
578
|
+
this.open = true;
|
|
579
|
+
this.dispatchEvent(new CustomEvent('open', {
|
|
580
|
+
bubbles: true,
|
|
581
|
+
detail: { trigger: fromKeyboard ? 'keyboard' : 'click' },
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
#onTriggerKey = (e) => {
|
|
587
|
+
if (this.disabled) return;
|
|
588
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
589
|
+
if (this.readonly) {
|
|
590
|
+
e.preventDefault();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
// Flag the synthetic click that <button-ui>'s own keydown handler
|
|
594
|
+
// will fire via `this.click()` — see comment on #keyboardOpen.
|
|
595
|
+
this.#keyboardOpen = true;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
#onPopoverClick = (e) => {
|
|
600
|
+
if (this.readonly) {
|
|
601
|
+
// Block preset / day clicks but allow the calendar widgets to
|
|
602
|
+
// remain focusable for screen-reader inspection.
|
|
603
|
+
const presetBtn = e.target.closest('[data-preset-label],[data-comparison-preset-label]');
|
|
604
|
+
const dayBtn = e.target.closest('[data-cal-day]');
|
|
605
|
+
if (presetBtn || dayBtn) {
|
|
606
|
+
e.stopPropagation();
|
|
607
|
+
e.preventDefault();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const presetBtn = e.target.closest('[data-preset-label]');
|
|
612
|
+
if (presetBtn) {
|
|
613
|
+
e.stopPropagation();
|
|
614
|
+
this.applyPreset(presetBtn.dataset.presetLabel);
|
|
615
|
+
this.closePopover('apply');
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const cmpPresetBtn = e.target.closest('[data-comparison-preset-label]');
|
|
619
|
+
if (cmpPresetBtn) {
|
|
620
|
+
e.stopPropagation();
|
|
621
|
+
// For v1, comparison-presets are tagged but compute their range
|
|
622
|
+
// from the current primary range. If no primary is set, no-op.
|
|
623
|
+
const primary = parseRange(this.value);
|
|
624
|
+
if (!primary) return;
|
|
625
|
+
const cmp = this.#computeComparisonRange(cmpPresetBtn.dataset.comparisonPresetLabel, primary);
|
|
626
|
+
if (cmp) this.rangeCompareValue = cmp;
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
#onPopoverKey = (e) => {
|
|
632
|
+
if (e.key === 'Escape') {
|
|
633
|
+
e.preventDefault();
|
|
634
|
+
e.stopPropagation();
|
|
635
|
+
this.closePopover('escape');
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (e.key === 'Tab') {
|
|
639
|
+
// Trap focus inside the popover. Build the tab-cycle by querying
|
|
640
|
+
// focusable descendants of the popover; the trigger is *outside*
|
|
641
|
+
// the popover and Tab cycles inside until Escape.
|
|
642
|
+
const focusables = this.#getFocusables();
|
|
643
|
+
if (focusables.length === 0) return;
|
|
644
|
+
const idx = focusables.indexOf(document.activeElement);
|
|
645
|
+
if (e.shiftKey) {
|
|
646
|
+
if (idx <= 0) {
|
|
647
|
+
e.preventDefault();
|
|
648
|
+
focusables[focusables.length - 1].focus();
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
if (idx === -1 || idx === focusables.length - 1) {
|
|
652
|
+
e.preventDefault();
|
|
653
|
+
focusables[0].focus();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
#onDocKey = (e) => {
|
|
660
|
+
// Belt-and-braces: Escape closes even if focus is outside the popover
|
|
661
|
+
// (e.g., the trigger).
|
|
662
|
+
if (e.key === 'Escape' && this.open) {
|
|
663
|
+
this.closePopover('escape');
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
#onCalFromChange = (e) => {
|
|
668
|
+
if (this.readonly) return;
|
|
669
|
+
e.stopPropagation();
|
|
670
|
+
const iso = e.detail?.value || this.#calFromRef?.value || '';
|
|
671
|
+
if (!iso) return;
|
|
672
|
+
const existing = parseRange(this.value);
|
|
673
|
+
if (!this.#pending) this.#pending = { from: '', to: '' };
|
|
674
|
+
this.#pending.from = iso;
|
|
675
|
+
// Live emit `input` for pending state.
|
|
676
|
+
this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: { from: iso, to: null } } }));
|
|
677
|
+
// Attempt commit when both halves are set (in either order). The
|
|
678
|
+
// commit path handles reversed-range invalidation.
|
|
679
|
+
const toCandidate = this.#pending.to || existing?.to || null;
|
|
680
|
+
if (toCandidate) {
|
|
681
|
+
this.#commitRange({ from: iso, to: toCandidate });
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
#onCalToChange = (e) => {
|
|
686
|
+
if (this.readonly) return;
|
|
687
|
+
e.stopPropagation();
|
|
688
|
+
const iso = e.detail?.value || this.#calToRef?.value || '';
|
|
689
|
+
if (!iso) return;
|
|
690
|
+
const existing = parseRange(this.value);
|
|
691
|
+
if (!this.#pending) this.#pending = { from: '', to: '' };
|
|
692
|
+
this.#pending.to = iso;
|
|
693
|
+
this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: { from: this.#pending.from || existing?.from || null, to: iso } } }));
|
|
694
|
+
const fromCandidate = this.#pending.from || existing?.from || null;
|
|
695
|
+
if (fromCandidate) {
|
|
696
|
+
this.#commitRange({ from: fromCandidate, to: iso });
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
#onOutside = (e) => {
|
|
701
|
+
if (!this.open) return;
|
|
702
|
+
// Click inside the host or the popover doesn't close.
|
|
703
|
+
if (this.contains(e.target)) return;
|
|
704
|
+
if (this.#popoverRef && e.composedPath?.().includes(this.#popoverRef)) return;
|
|
705
|
+
this.closePopover('outside');
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
// ── Commit + invalid emission ─────────────────────────────────────
|
|
709
|
+
|
|
710
|
+
#commitRange(range, extras = {}) {
|
|
711
|
+
if (!range || !range.from || !range.to) return;
|
|
712
|
+
if (!rangeIsOrdered(range)) {
|
|
713
|
+
this.dispatchEvent(new CustomEvent('invalid', {
|
|
714
|
+
bubbles: true,
|
|
715
|
+
detail: { value: range, reason: 'reversed' },
|
|
716
|
+
}));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (this.min && range.from < this.min) {
|
|
720
|
+
this.dispatchEvent(new CustomEvent('invalid', {
|
|
721
|
+
bubbles: true,
|
|
722
|
+
detail: { value: range, reason: 'below-min' },
|
|
723
|
+
}));
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (this.max && range.to > this.max) {
|
|
727
|
+
this.dispatchEvent(new CustomEvent('invalid', {
|
|
728
|
+
bubbles: true,
|
|
729
|
+
detail: { value: range, reason: 'above-max' },
|
|
730
|
+
}));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const serialized = JSON.stringify({ from: range.from, to: range.to });
|
|
734
|
+
this.value = serialized;
|
|
735
|
+
this.#pending = null;
|
|
736
|
+
this.syncValue(serialized);
|
|
737
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
738
|
+
bubbles: true,
|
|
739
|
+
detail: {
|
|
740
|
+
value: { from: range.from, to: range.to },
|
|
741
|
+
compareValue: parseRange(this.compareValue),
|
|
742
|
+
presetLabel: extras.presetLabel || null,
|
|
743
|
+
},
|
|
744
|
+
}));
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
#computeComparisonRange(label, primary) {
|
|
748
|
+
const from = parseISO(primary.from);
|
|
749
|
+
const to = parseISO(primary.to);
|
|
750
|
+
if (!from || !to) return null;
|
|
751
|
+
const days = Math.round((to - from) / 86400000) + 1;
|
|
752
|
+
switch (label) {
|
|
753
|
+
case 'Previous period': {
|
|
754
|
+
const cmpTo = new Date(from);
|
|
755
|
+
cmpTo.setDate(cmpTo.getDate() - 1);
|
|
756
|
+
const cmpFrom = new Date(cmpTo);
|
|
757
|
+
cmpFrom.setDate(cmpFrom.getDate() - (days - 1));
|
|
758
|
+
return { from: toISO(cmpFrom), to: toISO(cmpTo) };
|
|
759
|
+
}
|
|
760
|
+
case 'Same period last year': {
|
|
761
|
+
const cmpFrom = new Date(from);
|
|
762
|
+
cmpFrom.setFullYear(cmpFrom.getFullYear() - 1);
|
|
763
|
+
const cmpTo = new Date(to);
|
|
764
|
+
cmpTo.setFullYear(cmpTo.getFullYear() - 1);
|
|
765
|
+
return { from: toISO(cmpFrom), to: toISO(cmpTo) };
|
|
766
|
+
}
|
|
767
|
+
case 'Same period last month': {
|
|
768
|
+
const cmpFrom = new Date(from);
|
|
769
|
+
cmpFrom.setMonth(cmpFrom.getMonth() - 1);
|
|
770
|
+
const cmpTo = new Date(to);
|
|
771
|
+
cmpTo.setMonth(cmpTo.getMonth() - 1);
|
|
772
|
+
return { from: toISO(cmpFrom), to: toISO(cmpTo) };
|
|
773
|
+
}
|
|
774
|
+
default:
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
#getFocusables() {
|
|
780
|
+
if (!this.#popoverRef) return [];
|
|
781
|
+
const SEL = [
|
|
782
|
+
'button-ui:not([disabled])',
|
|
783
|
+
'button:not([disabled])',
|
|
784
|
+
'calendar-grid-ui:not([disabled])',
|
|
785
|
+
'[data-cal-day]:not([disabled]):not([data-outside])',
|
|
786
|
+
'[tabindex="0"]',
|
|
787
|
+
].join(',');
|
|
788
|
+
return Array.from(this.#popoverRef.querySelectorAll(SEL))
|
|
789
|
+
.filter((el) => el.offsetParent !== null || el.matches(':popover-open *'));
|
|
790
|
+
}
|
|
791
|
+
}
|