@hypoth-ui/cli 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +19 -115
- package/dist/{add-PDBC4JTE.js → add-V5PW73GC.js} +29 -17
- package/dist/{chunk-5LTQ2XVL.js → chunk-27CLUUVC.js} +0 -2
- package/dist/{chunk-YPKFYE45.js → chunk-NWIRSZUQ.js} +6 -13
- package/dist/{chunk-GJ6JOQ3Q.js → chunk-PBK72SJJ.js} +1 -1
- package/dist/{diff-BQEXG7HU.js → diff-776UATCA.js} +2 -2
- package/dist/index.js +5 -5
- package/dist/{init-7AZXYAPJ.js → init-GDU2PW7K.js} +10 -13
- package/dist/{list-X6ZLM2NQ.js → list-XDP5I537.js} +3 -3
- package/package.json +16 -12
- package/registry/components.json +1820 -206
- package/templates/accordion/index.tsx +266 -0
- package/templates/accordion/wc/accordion-content.ts +113 -0
- package/templates/accordion/wc/accordion-item.ts +111 -0
- package/templates/accordion/wc/accordion-trigger.ts +105 -0
- package/templates/accordion/wc/accordion.ts +213 -0
- package/templates/accordion/wc/index.ts +12 -0
- package/templates/alert/index.tsx +177 -0
- package/templates/alert/wc/alert.ts +167 -0
- package/templates/alert/wc/index.ts +1 -0
- package/templates/alert-dialog/index.tsx +360 -0
- package/templates/alert-dialog/wc/alert-dialog-action.ts +43 -0
- package/templates/alert-dialog/wc/alert-dialog-cancel.ts +43 -0
- package/templates/alert-dialog/wc/alert-dialog-content.ts +42 -0
- package/templates/alert-dialog/wc/alert-dialog-description.ts +34 -0
- package/templates/alert-dialog/wc/alert-dialog-footer.ts +25 -0
- package/templates/alert-dialog/wc/alert-dialog-header.ts +25 -0
- package/templates/alert-dialog/wc/alert-dialog-title.ts +34 -0
- package/templates/alert-dialog/wc/alert-dialog-trigger.ts +46 -0
- package/templates/alert-dialog/wc/alert-dialog.ts +302 -0
- package/templates/alert-dialog/wc/index.ts +13 -0
- package/templates/aspect-ratio/index.tsx +50 -0
- package/templates/aspect-ratio/wc/aspect-ratio.ts +78 -0
- package/templates/aspect-ratio/wc/index.ts +5 -0
- package/templates/avatar/avatar-group.tsx +88 -0
- package/templates/avatar/avatar.tsx +124 -0
- package/templates/avatar/index.tsx +33 -0
- package/templates/avatar/wc/avatar-group.ts +112 -0
- package/templates/avatar/wc/avatar.ts +184 -0
- package/templates/avatar/wc/index.ts +5 -0
- package/templates/badge/index.tsx +140 -0
- package/templates/badge/wc/badge.ts +119 -0
- package/templates/badge/wc/index.ts +9 -0
- package/templates/breadcrumb/index.tsx +157 -0
- package/templates/breadcrumb/wc/breadcrumb-item.ts +30 -0
- package/templates/breadcrumb/wc/breadcrumb-link.ts +70 -0
- package/templates/breadcrumb/wc/breadcrumb-list.ts +30 -0
- package/templates/breadcrumb/wc/breadcrumb-page.ts +32 -0
- package/templates/breadcrumb/wc/breadcrumb-separator.ts +31 -0
- package/templates/breadcrumb/wc/breadcrumb.ts +55 -0
- package/templates/breadcrumb/wc/index.ts +10 -0
- package/templates/button/button.tsx +119 -0
- package/templates/button/index.ts +1 -0
- package/templates/button/wc/button.ts +169 -0
- package/templates/calendar/index.tsx +149 -0
- package/templates/calendar/wc/calendar.ts +316 -0
- package/templates/calendar/wc/index.ts +4 -0
- package/templates/card/index.tsx +108 -0
- package/templates/card/wc/card-content.ts +25 -0
- package/templates/card/wc/card-footer.ts +25 -0
- package/templates/card/wc/card-header.ts +25 -0
- package/templates/card/wc/card.ts +43 -0
- package/templates/card/wc/index.ts +8 -0
- package/templates/checkbox/checkbox.tsx +85 -0
- package/templates/checkbox/wc/checkbox.ts +247 -0
- package/templates/collapsible/index.tsx +172 -0
- package/templates/collapsible/wc/collapsible-content.ts +97 -0
- package/templates/collapsible/wc/collapsible-trigger.ts +39 -0
- package/templates/collapsible/wc/collapsible.ts +143 -0
- package/templates/collapsible/wc/index.ts +7 -0
- package/templates/combobox/combobox-content.tsx +141 -0
- package/templates/combobox/combobox-context.ts +36 -0
- package/templates/combobox/combobox-empty.tsx +38 -0
- package/templates/combobox/combobox-input.tsx +159 -0
- package/templates/combobox/combobox-loading.tsx +38 -0
- package/templates/combobox/combobox-option.tsx +99 -0
- package/templates/combobox/combobox-root.tsx +207 -0
- package/templates/combobox/combobox-tag.tsx +62 -0
- package/templates/combobox/index.ts +62 -0
- package/templates/combobox/wc/combobox-content.ts +97 -0
- package/templates/combobox/wc/combobox-input.ts +134 -0
- package/templates/combobox/wc/combobox-option.ts +111 -0
- package/templates/combobox/wc/combobox-tag.ts +103 -0
- package/templates/combobox/wc/combobox.ts +981 -0
- package/templates/combobox/wc/index.ts +5 -0
- package/templates/command/index.tsx +279 -0
- package/templates/command/wc/command-empty.ts +24 -0
- package/templates/command/wc/command-group.ts +60 -0
- package/templates/command/wc/command-input.ts +136 -0
- package/templates/command/wc/command-item.ts +78 -0
- package/templates/command/wc/command-list.ts +103 -0
- package/templates/command/wc/command-loading.ts +24 -0
- package/templates/command/wc/command-separator.ts +23 -0
- package/templates/command/wc/command.ts +176 -0
- package/templates/context-menu/index.tsx +262 -0
- package/templates/context-menu/wc/context-menu-content.ts +41 -0
- package/templates/context-menu/wc/context-menu-item.ts +83 -0
- package/templates/context-menu/wc/context-menu-label.ts +30 -0
- package/templates/context-menu/wc/context-menu-separator.ts +28 -0
- package/templates/context-menu/wc/context-menu.ts +324 -0
- package/templates/context-menu/wc/index.ts +9 -0
- package/templates/data-table/index.tsx +263 -0
- package/templates/data-table/wc/data-table.ts +405 -0
- package/templates/data-table/wc/index.ts +10 -0
- package/templates/date-picker/date-picker-calendar.tsx +352 -0
- package/templates/date-picker/date-picker-content.tsx +121 -0
- package/templates/date-picker/date-picker-context.ts +46 -0
- package/templates/date-picker/date-picker-root.tsx +201 -0
- package/templates/date-picker/date-picker-trigger.tsx +95 -0
- package/templates/date-picker/index.ts +44 -0
- package/templates/date-picker/wc/date-picker-calendar.ts +457 -0
- package/templates/date-picker/wc/date-picker.ts +592 -0
- package/templates/date-picker/wc/date-utils.ts +467 -0
- package/templates/date-picker/wc/index.ts +3 -0
- package/templates/dialog/dialog-close.tsx +57 -0
- package/templates/dialog/dialog-content.tsx +106 -0
- package/templates/dialog/dialog-context.ts +24 -0
- package/templates/dialog/dialog-description.tsx +51 -0
- package/templates/dialog/dialog-root.tsx +104 -0
- package/templates/dialog/dialog-title.tsx +38 -0
- package/templates/dialog/dialog-trigger.tsx +94 -0
- package/templates/dialog/index.ts +52 -0
- package/templates/dialog/wc/dialog-content.ts +59 -0
- package/templates/dialog/wc/dialog-description.ts +58 -0
- package/templates/dialog/wc/dialog-title.ts +56 -0
- package/templates/dialog/wc/dialog.ts +411 -0
- package/templates/drawer/index.tsx +263 -0
- package/templates/drawer/wc/drawer-content.ts +150 -0
- package/templates/drawer/wc/drawer-description.ts +34 -0
- package/templates/drawer/wc/drawer-footer.ts +25 -0
- package/templates/drawer/wc/drawer-header.ts +25 -0
- package/templates/drawer/wc/drawer-title.ts +34 -0
- package/templates/drawer/wc/drawer.ts +348 -0
- package/templates/drawer/wc/index.ts +10 -0
- package/templates/dropdown-menu/index.tsx +454 -0
- package/templates/dropdown-menu/wc/dropdown-menu-checkbox-item.ts +93 -0
- package/templates/dropdown-menu/wc/dropdown-menu-content.ts +43 -0
- package/templates/dropdown-menu/wc/dropdown-menu-item.ts +85 -0
- package/templates/dropdown-menu/wc/dropdown-menu-label.ts +31 -0
- package/templates/dropdown-menu/wc/dropdown-menu-radio-group.ts +80 -0
- package/templates/dropdown-menu/wc/dropdown-menu-radio-item.ts +101 -0
- package/templates/dropdown-menu/wc/dropdown-menu-separator.ts +28 -0
- package/templates/dropdown-menu/wc/dropdown-menu.ts +358 -0
- package/templates/dropdown-menu/wc/index.ts +12 -0
- package/templates/field/field-description.tsx +39 -0
- package/templates/field/field-error.tsx +37 -0
- package/templates/field/field.tsx +46 -0
- package/templates/field/index.ts +4 -0
- package/templates/field/label.tsx +40 -0
- package/templates/field/wc/field-description.ts +42 -0
- package/templates/field/wc/field-error.ts +46 -0
- package/templates/field/wc/field.ts +210 -0
- package/templates/field/wc/label.ts +54 -0
- package/templates/file-upload/file-upload-context.ts +26 -0
- package/templates/file-upload/file-upload-dropzone.tsx +111 -0
- package/templates/file-upload/file-upload-input.tsx +86 -0
- package/templates/file-upload/file-upload-item.tsx +105 -0
- package/templates/file-upload/file-upload-root.tsx +115 -0
- package/templates/file-upload/index.ts +50 -0
- package/templates/file-upload/wc/file-upload.ts +380 -0
- package/templates/file-upload/wc/index.ts +1 -0
- package/templates/hover-card/index.tsx +203 -0
- package/templates/hover-card/wc/hover-card-content.ts +50 -0
- package/templates/hover-card/wc/hover-card.ts +382 -0
- package/templates/hover-card/wc/index.ts +6 -0
- package/templates/icon/icon.tsx +76 -0
- package/templates/icon/wc/icon-adapter.ts +108 -0
- package/templates/icon/wc/icon.ts +161 -0
- package/templates/input/input.tsx +130 -0
- package/templates/input/wc/input.ts +216 -0
- package/templates/layout/app-shell.tsx +177 -0
- package/templates/layout/box.tsx +53 -0
- package/templates/layout/center.tsx +42 -0
- package/templates/layout/container.tsx +43 -0
- package/templates/layout/flow.tsx +83 -0
- package/templates/layout/grid.tsx +79 -0
- package/templates/layout/index.ts +33 -0
- package/templates/layout/inline.tsx +16 -0
- package/templates/layout/page.tsx +43 -0
- package/templates/layout/section.tsx +39 -0
- package/templates/layout/spacer.tsx +30 -0
- package/templates/layout/split.tsx +47 -0
- package/templates/layout/stack.tsx +16 -0
- package/templates/layout/wc/app-shell.ts +58 -0
- package/templates/layout/wc/box.ts +117 -0
- package/templates/layout/wc/center.ts +78 -0
- package/templates/layout/wc/container.ts +77 -0
- package/templates/layout/wc/flow.ts +149 -0
- package/templates/layout/wc/footer.ts +57 -0
- package/templates/layout/wc/grid.ts +142 -0
- package/templates/layout/wc/header.ts +57 -0
- package/templates/layout/wc/index.ts +41 -0
- package/templates/layout/wc/main.ts +46 -0
- package/templates/layout/wc/page.ts +81 -0
- package/templates/layout/wc/section.ts +65 -0
- package/templates/layout/wc/spacer.ts +77 -0
- package/templates/layout/wc/split.ts +94 -0
- package/templates/layout/wc/wrap.ts +93 -0
- package/templates/layout/wrap.tsx +46 -0
- package/templates/link/link.tsx +109 -0
- package/templates/link/wc/link.ts +124 -0
- package/templates/list/index.tsx +55 -0
- package/templates/list/list-item.tsx +117 -0
- package/templates/list/list.tsx +115 -0
- package/templates/list/wc/index.ts +5 -0
- package/templates/list/wc/list-item.ts +127 -0
- package/templates/list/wc/list.ts +114 -0
- package/templates/menu/index.ts +49 -0
- package/templates/menu/menu-content.tsx +109 -0
- package/templates/menu/menu-context.ts +17 -0
- package/templates/menu/menu-item.tsx +108 -0
- package/templates/menu/menu-label.tsx +32 -0
- package/templates/menu/menu-root.tsx +108 -0
- package/templates/menu/menu-separator.tsx +24 -0
- package/templates/menu/menu-trigger.tsx +104 -0
- package/templates/menu/wc/menu-content.ts +67 -0
- package/templates/menu/wc/menu-item.ts +109 -0
- package/templates/menu/wc/menu.ts +449 -0
- package/templates/navigation-menu/index.tsx +328 -0
- package/templates/navigation-menu/wc/index.ts +12 -0
- package/templates/navigation-menu/wc/navigation-menu-content.ts +30 -0
- package/templates/navigation-menu/wc/navigation-menu-indicator.ts +30 -0
- package/templates/navigation-menu/wc/navigation-menu-item.ts +60 -0
- package/templates/navigation-menu/wc/navigation-menu-link.ts +97 -0
- package/templates/navigation-menu/wc/navigation-menu-list.ts +30 -0
- package/templates/navigation-menu/wc/navigation-menu-trigger.ts +110 -0
- package/templates/navigation-menu/wc/navigation-menu-viewport.ts +85 -0
- package/templates/navigation-menu/wc/navigation-menu.ts +272 -0
- package/templates/number-input/index.ts +46 -0
- package/templates/number-input/number-input-context.ts +38 -0
- package/templates/number-input/number-input-decrement.tsx +53 -0
- package/templates/number-input/number-input-field.tsx +93 -0
- package/templates/number-input/number-input-increment.tsx +53 -0
- package/templates/number-input/number-input-root.tsx +137 -0
- package/templates/number-input/wc/index.ts +1 -0
- package/templates/number-input/wc/number-input.ts +283 -0
- package/templates/pagination/index.tsx +198 -0
- package/templates/pagination/wc/index.ts +11 -0
- package/templates/pagination/wc/pagination-content.ts +30 -0
- package/templates/pagination/wc/pagination-ellipsis.ts +28 -0
- package/templates/pagination/wc/pagination-item.ts +30 -0
- package/templates/pagination/wc/pagination-link.ts +76 -0
- package/templates/pagination/wc/pagination-next.ts +69 -0
- package/templates/pagination/wc/pagination-previous.ts +69 -0
- package/templates/pagination/wc/pagination.ts +156 -0
- package/templates/pin-input/index.ts +39 -0
- package/templates/pin-input/pin-input-context.ts +30 -0
- package/templates/pin-input/pin-input-field.tsx +186 -0
- package/templates/pin-input/pin-input-root.tsx +120 -0
- package/templates/pin-input/wc/index.ts +1 -0
- package/templates/pin-input/wc/pin-input.ts +259 -0
- package/templates/popover/popover.tsx +121 -0
- package/templates/popover/wc/popover-content.ts +66 -0
- package/templates/popover/wc/popover.ts +343 -0
- package/templates/progress/index.tsx +117 -0
- package/templates/progress/wc/index.ts +4 -0
- package/templates/progress/wc/progress.ts +174 -0
- package/templates/radio/radio.tsx +43 -0
- package/templates/radio/wc/radio-group.ts +261 -0
- package/templates/radio/wc/radio.ts +145 -0
- package/templates/scroll-area/index.tsx +144 -0
- package/templates/scroll-area/wc/index.ts +8 -0
- package/templates/scroll-area/wc/scroll-area-scrollbar.ts +143 -0
- package/templates/scroll-area/wc/scroll-area-thumb.ts +225 -0
- package/templates/scroll-area/wc/scroll-area-viewport.ts +120 -0
- package/templates/scroll-area/wc/scroll-area.ts +63 -0
- package/templates/select/index.ts +57 -0
- package/templates/select/select-content.tsx +243 -0
- package/templates/select/select-context.ts +30 -0
- package/templates/select/select-group.tsx +53 -0
- package/templates/select/select-label.tsx +34 -0
- package/templates/select/select-option.tsx +97 -0
- package/templates/select/select-root.tsx +153 -0
- package/templates/select/select-separator.tsx +27 -0
- package/templates/select/select-trigger.tsx +112 -0
- package/templates/select/select-value.tsx +48 -0
- package/templates/select/wc/index.ts +6 -0
- package/templates/select/wc/select-content.ts +89 -0
- package/templates/select/wc/select-group.ts +82 -0
- package/templates/select/wc/select-label.ts +49 -0
- package/templates/select/wc/select-option.ts +111 -0
- package/templates/select/wc/select-trigger.ts +101 -0
- package/templates/select/wc/select.ts +840 -0
- package/templates/separator/index.tsx +49 -0
- package/templates/separator/wc/index.ts +5 -0
- package/templates/separator/wc/separator.ts +60 -0
- package/templates/sheet/index.tsx +291 -0
- package/templates/sheet/wc/index.ts +12 -0
- package/templates/sheet/wc/sheet-close.ts +43 -0
- package/templates/sheet/wc/sheet-content.ts +47 -0
- package/templates/sheet/wc/sheet-description.ts +34 -0
- package/templates/sheet/wc/sheet-footer.ts +25 -0
- package/templates/sheet/wc/sheet-header.ts +25 -0
- package/templates/sheet/wc/sheet-overlay.ts +23 -0
- package/templates/sheet/wc/sheet-title.ts +34 -0
- package/templates/sheet/wc/sheet.ts +336 -0
- package/templates/skeleton/index.tsx +131 -0
- package/templates/skeleton/wc/index.ts +10 -0
- package/templates/skeleton/wc/skeleton.ts +107 -0
- package/templates/slider/index.ts +41 -0
- package/templates/slider/slider-context.ts +36 -0
- package/templates/slider/slider-range.tsx +59 -0
- package/templates/slider/slider-root.tsx +166 -0
- package/templates/slider/slider-thumb.tsx +213 -0
- package/templates/slider/slider-track.tsx +113 -0
- package/templates/slider/wc/index.ts +1 -0
- package/templates/slider/wc/slider.ts +465 -0
- package/templates/spinner/spinner.tsx +64 -0
- package/templates/spinner/wc/spinner.ts +70 -0
- package/templates/stepper/index.tsx +230 -0
- package/templates/stepper/wc/index.ts +12 -0
- package/templates/stepper/wc/stepper-content.ts +30 -0
- package/templates/stepper/wc/stepper-description.ts +25 -0
- package/templates/stepper/wc/stepper-indicator.ts +30 -0
- package/templates/stepper/wc/stepper-item.ts +55 -0
- package/templates/stepper/wc/stepper-separator.ts +29 -0
- package/templates/stepper/wc/stepper-title.ts +25 -0
- package/templates/stepper/wc/stepper-trigger.ts +67 -0
- package/templates/stepper/wc/stepper.ts +164 -0
- package/templates/switch/switch.tsx +90 -0
- package/templates/switch/wc/switch.ts +228 -0
- package/templates/table/body.tsx +21 -0
- package/templates/table/cell.tsx +44 -0
- package/templates/table/head.tsx +112 -0
- package/templates/table/header.tsx +21 -0
- package/templates/table/index.tsx +93 -0
- package/templates/table/root.tsx +82 -0
- package/templates/table/row.tsx +36 -0
- package/templates/table/wc/index.ts +9 -0
- package/templates/table/wc/table-body.ts +32 -0
- package/templates/table/wc/table-cell.ts +58 -0
- package/templates/table/wc/table-head.ts +129 -0
- package/templates/table/wc/table-header.ts +32 -0
- package/templates/table/wc/table-row.ts +50 -0
- package/templates/table/wc/table.ts +93 -0
- package/templates/tabs/index.tsx +222 -0
- package/templates/tabs/wc/index.ts +8 -0
- package/templates/tabs/wc/tabs-content.ts +82 -0
- package/templates/tabs/wc/tabs-list.ts +56 -0
- package/templates/tabs/wc/tabs-trigger.ts +136 -0
- package/templates/tabs/wc/tabs.ts +202 -0
- package/templates/tag/index.tsx +186 -0
- package/templates/tag/wc/index.ts +4 -0
- package/templates/tag/wc/tag.ts +166 -0
- package/templates/text/text.tsx +100 -0
- package/templates/text/wc/text.ts +94 -0
- package/templates/textarea/textarea.tsx +134 -0
- package/templates/textarea/wc/textarea.ts +280 -0
- package/templates/time-picker/index.ts +42 -0
- package/templates/time-picker/time-picker-context.ts +28 -0
- package/templates/time-picker/time-picker-root.tsx +113 -0
- package/templates/time-picker/time-picker-segment.tsx +91 -0
- package/templates/time-picker/wc/index.ts +1 -0
- package/templates/time-picker/wc/time-picker.ts +221 -0
- package/templates/toast/index.tsx +71 -0
- package/templates/toast/provider.tsx +228 -0
- package/templates/toast/toast.tsx +142 -0
- package/templates/toast/use-toast.ts +89 -0
- package/templates/toast/wc/index.ts +15 -0
- package/templates/toast/wc/toast-controller.ts +282 -0
- package/templates/toast/wc/toast-provider.ts +161 -0
- package/templates/toast/wc/toast.ts +165 -0
- package/templates/tooltip/tooltip.tsx +62 -0
- package/templates/tooltip/wc/tooltip-content.ts +64 -0
- package/templates/tooltip/wc/tooltip.ts +289 -0
- package/templates/tree/index.tsx +60 -0
- package/templates/tree/tree-item.tsx +131 -0
- package/templates/tree/tree.tsx +138 -0
- package/templates/tree/wc/index.ts +11 -0
- package/templates/tree/wc/tree-item.ts +273 -0
- package/templates/tree/wc/tree-utils.ts +143 -0
- package/templates/tree/wc/tree.ts +139 -0
- package/templates/visually-hidden/visually-hidden.tsx +45 -0
- package/templates/visually-hidden/wc/visually-hidden.ts +64 -0
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnchorPosition,
|
|
3
|
+
type ComboboxBehavior,
|
|
4
|
+
type DismissableLayer,
|
|
5
|
+
type Option,
|
|
6
|
+
type Placement,
|
|
7
|
+
type Presence,
|
|
8
|
+
type RovingFocus,
|
|
9
|
+
type VirtualizedList,
|
|
10
|
+
createAnchorPosition,
|
|
11
|
+
createComboboxBehavior,
|
|
12
|
+
createDismissableLayer,
|
|
13
|
+
createPresence,
|
|
14
|
+
createRovingFocus,
|
|
15
|
+
prefersReducedMotion,
|
|
16
|
+
} from "@hypoth-ui/primitives-dom";
|
|
17
|
+
import { html, nothing } from "lit";
|
|
18
|
+
import type { PropertyValues } from "lit";
|
|
19
|
+
import { property, state } from "lit/decorators.js";
|
|
20
|
+
import { repeat } from "lit/directives/repeat.js";
|
|
21
|
+
import { DSElement } from "../../base/ds-element.js";
|
|
22
|
+
import { FormAssociatedMixin } from "../../base/form-associated.js";
|
|
23
|
+
import type { ValidationFlags } from "../../base/form-associated.js";
|
|
24
|
+
import { StandardEvents, emitEvent } from "../../events/emit.js";
|
|
25
|
+
import { define } from "../../registry/define.js";
|
|
26
|
+
|
|
27
|
+
// Import child components to ensure they're registered
|
|
28
|
+
import type { DsComboboxContent } from "./combobox-content.js";
|
|
29
|
+
import type { DsComboboxInput } from "./combobox-input.js";
|
|
30
|
+
import type { DsComboboxOption } from "./combobox-option.js";
|
|
31
|
+
import "./combobox-content.js";
|
|
32
|
+
import "./combobox-input.js";
|
|
33
|
+
import "./combobox-option.js";
|
|
34
|
+
import "./combobox-tag.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Combobox component with async loading, multi-select, keyboard navigation, and native form participation.
|
|
38
|
+
*
|
|
39
|
+
* Uses ElementInternals for form association - the selected value(s) are submitted with the form
|
|
40
|
+
* and the combobox participates in constraint validation.
|
|
41
|
+
*
|
|
42
|
+
* Implements WAI-ARIA Combobox pattern with:
|
|
43
|
+
* - Text input with autocomplete suggestions
|
|
44
|
+
* - Arrow key navigation between options
|
|
45
|
+
* - Async loading with debounce
|
|
46
|
+
* - Multi-select with tag display
|
|
47
|
+
* - Enter to select, Escape to close
|
|
48
|
+
*
|
|
49
|
+
* @element ds-combobox
|
|
50
|
+
*
|
|
51
|
+
* @slot input - Input element (ds-combobox-input with input inside)
|
|
52
|
+
* @slot tags - Selected value tags (for multi-select)
|
|
53
|
+
* @slot - Combobox content (ds-combobox-content with ds-combobox-option children)
|
|
54
|
+
*
|
|
55
|
+
* @fires ds:open - Fired when combobox opens
|
|
56
|
+
* @fires ds:close - Fired when combobox closes
|
|
57
|
+
* @fires ds:change - Fired when value changes (detail: { value, label } or { values, labels })
|
|
58
|
+
* @fires ds:input - Fired when input value changes (detail: { value })
|
|
59
|
+
* @fires ds:invalid - Fired when customValidation is true and validation fails
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```html
|
|
63
|
+
* <form>
|
|
64
|
+
* <ds-combobox name="fruit" required>
|
|
65
|
+
* <ds-combobox-input slot="input">
|
|
66
|
+
* <input placeholder="Search fruits..." />
|
|
67
|
+
* </ds-combobox-input>
|
|
68
|
+
* <ds-combobox-content>
|
|
69
|
+
* <ds-combobox-option value="apple">Apple</ds-combobox-option>
|
|
70
|
+
* <ds-combobox-option value="banana">Banana</ds-combobox-option>
|
|
71
|
+
* </ds-combobox-content>
|
|
72
|
+
* </ds-combobox>
|
|
73
|
+
* <button type="submit">Submit</button>
|
|
74
|
+
* </form>
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export class DsCombobox extends FormAssociatedMixin(DSElement) {
|
|
78
|
+
/** Whether the combobox is open */
|
|
79
|
+
@property({ type: Boolean, reflect: true })
|
|
80
|
+
open = false;
|
|
81
|
+
|
|
82
|
+
/** Current selected value (single-select mode) */
|
|
83
|
+
@property({ type: String, reflect: true })
|
|
84
|
+
value = "";
|
|
85
|
+
|
|
86
|
+
/** Current selected values (multi-select mode) */
|
|
87
|
+
@property({ type: Array })
|
|
88
|
+
values: string[] = [];
|
|
89
|
+
|
|
90
|
+
/** Placement relative to input */
|
|
91
|
+
@property({ type: String, reflect: true })
|
|
92
|
+
placement: Placement = "bottom-start";
|
|
93
|
+
|
|
94
|
+
/** Offset distance from input in pixels */
|
|
95
|
+
@property({ type: Number })
|
|
96
|
+
offset = 4;
|
|
97
|
+
|
|
98
|
+
/** Whether to flip placement when near viewport edge */
|
|
99
|
+
@property({ type: Boolean })
|
|
100
|
+
flip = true;
|
|
101
|
+
|
|
102
|
+
/** Whether to animate open/close transitions */
|
|
103
|
+
@property({ type: Boolean })
|
|
104
|
+
animated = true;
|
|
105
|
+
|
|
106
|
+
/** Whether the combobox is disabled */
|
|
107
|
+
@property({ type: Boolean, reflect: true })
|
|
108
|
+
disabled = false;
|
|
109
|
+
|
|
110
|
+
/** Enable multi-select mode */
|
|
111
|
+
@property({ type: Boolean, reflect: true })
|
|
112
|
+
multiple = false;
|
|
113
|
+
|
|
114
|
+
/** Allow creating new values */
|
|
115
|
+
@property({ type: Boolean })
|
|
116
|
+
creatable = false;
|
|
117
|
+
|
|
118
|
+
/** Debounce delay for async loading in milliseconds */
|
|
119
|
+
@property({ type: Number })
|
|
120
|
+
debounce = 300;
|
|
121
|
+
|
|
122
|
+
/** Virtualization threshold (default: 100) */
|
|
123
|
+
@property({ type: Number, attribute: "virtualization-threshold" })
|
|
124
|
+
virtualizationThreshold = 100;
|
|
125
|
+
|
|
126
|
+
/** Enable virtualization for large lists */
|
|
127
|
+
@property({ type: Boolean })
|
|
128
|
+
virtualize = false;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Async loading function. Called when input changes (after debounce).
|
|
132
|
+
* Return an array of Option objects: { value: string, label: string, disabled?: boolean }
|
|
133
|
+
*/
|
|
134
|
+
@property({ attribute: false })
|
|
135
|
+
loadItems?: (query: string, signal: AbortSignal) => Promise<Option<string>[]>;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Data-driven options array. Use this for programmatic option rendering.
|
|
139
|
+
* When provided, options will be rendered from this array instead of slots.
|
|
140
|
+
*/
|
|
141
|
+
@property({ attribute: false })
|
|
142
|
+
items: Option<string>[] = [];
|
|
143
|
+
|
|
144
|
+
/** Whether async loading is in progress (read-only) */
|
|
145
|
+
@state()
|
|
146
|
+
loading = false;
|
|
147
|
+
|
|
148
|
+
/** Error message from async loading (read-only) */
|
|
149
|
+
@state()
|
|
150
|
+
loadError: string | null = null;
|
|
151
|
+
|
|
152
|
+
/** Filtered items for display */
|
|
153
|
+
@state()
|
|
154
|
+
private filteredItems: Option<string>[] = [];
|
|
155
|
+
|
|
156
|
+
/** Visible item IDs for virtualization */
|
|
157
|
+
@state()
|
|
158
|
+
private visibleItemIds = new Set<string>();
|
|
159
|
+
|
|
160
|
+
/** Default value for form reset (single-select) */
|
|
161
|
+
private _defaultValue = "";
|
|
162
|
+
|
|
163
|
+
/** Default values for form reset (multi-select) */
|
|
164
|
+
private _defaultValues: string[] = [];
|
|
165
|
+
|
|
166
|
+
private behavior: ComboboxBehavior<string, false> | ComboboxBehavior<string, true> | null = null;
|
|
167
|
+
private anchorPosition: AnchorPosition | null = null;
|
|
168
|
+
private dismissLayer: DismissableLayer | null = null;
|
|
169
|
+
private presence: Presence | null = null;
|
|
170
|
+
private rovingFocus: RovingFocus | null = null;
|
|
171
|
+
private virtualizedList: VirtualizedList | null = null;
|
|
172
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
173
|
+
private scrollHandler: (() => void) | null = null;
|
|
174
|
+
private loadAbortController: AbortController | null = null;
|
|
175
|
+
private debounceTimeout: number | null = null;
|
|
176
|
+
|
|
177
|
+
override connectedCallback(): void {
|
|
178
|
+
// Store default value(s) for form reset
|
|
179
|
+
this._defaultValue = this.value;
|
|
180
|
+
this._defaultValues = [...this.values];
|
|
181
|
+
|
|
182
|
+
super.connectedCallback();
|
|
183
|
+
|
|
184
|
+
// Initialize behavior based on multiple prop
|
|
185
|
+
this.initBehavior();
|
|
186
|
+
|
|
187
|
+
// Listen for input interactions
|
|
188
|
+
this.addEventListener("input", this.handleInput);
|
|
189
|
+
this.addEventListener("keydown", this.handleKeyDown);
|
|
190
|
+
this.addEventListener("click", this.handleClick);
|
|
191
|
+
|
|
192
|
+
// Listen for tag removal
|
|
193
|
+
this.addEventListener("ds:remove", this.handleTagRemove);
|
|
194
|
+
|
|
195
|
+
// Setup after first render
|
|
196
|
+
this.updateComplete.then(() => {
|
|
197
|
+
this.setupInputAccessibility();
|
|
198
|
+
this.registerOptions();
|
|
199
|
+
this.updateOptionStates();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
override disconnectedCallback(): void {
|
|
204
|
+
super.disconnectedCallback();
|
|
205
|
+
this.removeEventListener("input", this.handleInput);
|
|
206
|
+
this.removeEventListener("keydown", this.handleKeyDown);
|
|
207
|
+
this.removeEventListener("click", this.handleClick);
|
|
208
|
+
this.removeEventListener("ds:remove", this.handleTagRemove);
|
|
209
|
+
this.cleanup();
|
|
210
|
+
this.behavior?.destroy();
|
|
211
|
+
this.behavior = null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private initBehavior(): void {
|
|
215
|
+
if (this.multiple) {
|
|
216
|
+
this.behavior = createComboboxBehavior<string, true>({
|
|
217
|
+
defaultValue: this.values,
|
|
218
|
+
multiple: true,
|
|
219
|
+
creatable: this.creatable,
|
|
220
|
+
debounce: this.debounce,
|
|
221
|
+
virtualizationThreshold: this.virtualizationThreshold,
|
|
222
|
+
disabled: this.disabled,
|
|
223
|
+
onValueChange: (values) => {
|
|
224
|
+
this.values = values;
|
|
225
|
+
const options = this.getOptions();
|
|
226
|
+
const labels = values.map((v) => {
|
|
227
|
+
const opt = options.find((o) => o.value === v);
|
|
228
|
+
return opt?.getLabel() ?? v;
|
|
229
|
+
});
|
|
230
|
+
emitEvent(this, StandardEvents.CHANGE, { detail: { values, labels } });
|
|
231
|
+
},
|
|
232
|
+
onInputChange: (query) => {
|
|
233
|
+
emitEvent(this, "input", { detail: { value: query } });
|
|
234
|
+
this.filterOptions(query);
|
|
235
|
+
},
|
|
236
|
+
onCreateValue: (value) => {
|
|
237
|
+
emitEvent(this, "create", { detail: { value } });
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
this.behavior = createComboboxBehavior<string, false>({
|
|
242
|
+
defaultValue: this.value || null,
|
|
243
|
+
multiple: false,
|
|
244
|
+
creatable: this.creatable,
|
|
245
|
+
debounce: this.debounce,
|
|
246
|
+
virtualizationThreshold: this.virtualizationThreshold,
|
|
247
|
+
disabled: this.disabled,
|
|
248
|
+
onValueChange: (value) => {
|
|
249
|
+
this.value = value ?? "";
|
|
250
|
+
const option = this.getOptionByValue(value ?? "");
|
|
251
|
+
emitEvent(this, StandardEvents.CHANGE, {
|
|
252
|
+
detail: {
|
|
253
|
+
value: value ?? "",
|
|
254
|
+
label: option?.getLabel() ?? "",
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
onInputChange: (query) => {
|
|
259
|
+
emitEvent(this, "input", { detail: { value: query } });
|
|
260
|
+
this.filterOptions(query);
|
|
261
|
+
},
|
|
262
|
+
onCreateValue: (value) => {
|
|
263
|
+
emitEvent(this, "create", { detail: { value } });
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Opens the combobox.
|
|
271
|
+
*/
|
|
272
|
+
public show(): void {
|
|
273
|
+
if (this.open || this.disabled) return;
|
|
274
|
+
this.behavior?.open();
|
|
275
|
+
this.open = true;
|
|
276
|
+
emitEvent(this, StandardEvents.OPEN);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Closes the combobox.
|
|
281
|
+
*/
|
|
282
|
+
public close(): void {
|
|
283
|
+
if (!this.open) return;
|
|
284
|
+
|
|
285
|
+
const content = this.querySelector("ds-combobox-content") as DsComboboxContent | null;
|
|
286
|
+
|
|
287
|
+
// If animated, use presence for exit animation
|
|
288
|
+
if (this.animated && content && !prefersReducedMotion()) {
|
|
289
|
+
this.dismissLayer?.deactivate();
|
|
290
|
+
this.dismissLayer = null;
|
|
291
|
+
|
|
292
|
+
this.presence = createPresence({
|
|
293
|
+
onExitComplete: () => {
|
|
294
|
+
this.completeClose();
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
this.presence.hide(content);
|
|
298
|
+
} else {
|
|
299
|
+
this.cleanup();
|
|
300
|
+
this.behavior?.close();
|
|
301
|
+
this.open = false;
|
|
302
|
+
emitEvent(this, StandardEvents.CLOSE);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private completeClose(): void {
|
|
307
|
+
this.cleanup();
|
|
308
|
+
this.behavior?.close();
|
|
309
|
+
this.open = false;
|
|
310
|
+
emitEvent(this, StandardEvents.CLOSE);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Selects a value programmatically.
|
|
315
|
+
*/
|
|
316
|
+
public select(value: string): void {
|
|
317
|
+
if (this.disabled) return;
|
|
318
|
+
this.behavior?.select(value);
|
|
319
|
+
this.updateOptionStates();
|
|
320
|
+
if (!this.multiple) {
|
|
321
|
+
this.close();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Removes a value (multi-select mode).
|
|
327
|
+
*/
|
|
328
|
+
public removeValue(value: string): void {
|
|
329
|
+
if (this.disabled || !this.multiple) return;
|
|
330
|
+
(this.behavior as ComboboxBehavior<string, true>)?.remove(value);
|
|
331
|
+
this.updateOptionStates();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Clears all selections.
|
|
336
|
+
*/
|
|
337
|
+
public clear(): void {
|
|
338
|
+
if (this.disabled) return;
|
|
339
|
+
this.behavior?.clear();
|
|
340
|
+
this.updateOptionStates();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Creates a new value (creatable mode).
|
|
345
|
+
*/
|
|
346
|
+
public create(value: string): void {
|
|
347
|
+
if (!this.creatable || this.disabled) return;
|
|
348
|
+
this.behavior?.create(value);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private getInputWrapper(): DsComboboxInput | null {
|
|
352
|
+
return this.querySelector("ds-combobox-input") as DsComboboxInput | null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private getInputElement(): HTMLInputElement | null {
|
|
356
|
+
return this.getInputWrapper()?.getInputElement() ?? null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private getOptions(): DsComboboxOption[] {
|
|
360
|
+
const content = this.querySelector("ds-combobox-content");
|
|
361
|
+
if (!content) return [];
|
|
362
|
+
return Array.from(content.querySelectorAll<DsComboboxOption>("ds-combobox-option"));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private getEnabledOptions(): DsComboboxOption[] {
|
|
366
|
+
return this.getOptions().filter((opt) => !opt.disabled);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private getOptionByValue(value: string): DsComboboxOption | null {
|
|
370
|
+
return this.getOptions().find((opt) => opt.value === value) ?? null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private registerOptions(): void {
|
|
374
|
+
const options = this.getOptions();
|
|
375
|
+
const _items: Option<string>[] = options.map((opt) => ({
|
|
376
|
+
value: opt.value,
|
|
377
|
+
label: opt.getLabel(),
|
|
378
|
+
disabled: opt.disabled,
|
|
379
|
+
}));
|
|
380
|
+
// Set static items on behavior (for filtering)
|
|
381
|
+
// Note: behavior stores items internally via the items option
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private filterOptions(query: string): void {
|
|
385
|
+
const options = this.getOptions();
|
|
386
|
+
const lowerQuery = query.toLowerCase();
|
|
387
|
+
|
|
388
|
+
for (const option of options) {
|
|
389
|
+
const label = option.getLabel().toLowerCase();
|
|
390
|
+
const matches = !query || label.includes(lowerQuery);
|
|
391
|
+
|
|
392
|
+
if (matches) {
|
|
393
|
+
option.removeAttribute("hidden");
|
|
394
|
+
} else {
|
|
395
|
+
option.setAttribute("hidden", "");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Auto-highlight first visible option
|
|
400
|
+
const visibleOptions = this.getEnabledOptions().filter((opt) => !opt.hasAttribute("hidden"));
|
|
401
|
+
if (visibleOptions.length > 0 && visibleOptions[0]) {
|
|
402
|
+
this.behavior?.highlightFirst();
|
|
403
|
+
this.updateOptionStates();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private updateOptionStates(): void {
|
|
408
|
+
const currentValue = this.multiple
|
|
409
|
+
? ((this.behavior as ComboboxBehavior<string, true>)?.state.value ?? this.values)
|
|
410
|
+
: ([this.behavior?.state.value ?? this.value].filter(Boolean) as string[]);
|
|
411
|
+
const highlightedValue = this.behavior?.state.highlightedValue;
|
|
412
|
+
|
|
413
|
+
for (const option of this.getOptions()) {
|
|
414
|
+
const isSelected = currentValue.includes(option.value);
|
|
415
|
+
option.setSelected(isSelected);
|
|
416
|
+
option.setHighlighted(option.value === highlightedValue);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private handleInput = (event: Event): void => {
|
|
421
|
+
const target = event.target as HTMLInputElement;
|
|
422
|
+
if (target.tagName !== "INPUT") return;
|
|
423
|
+
|
|
424
|
+
const value = target.value;
|
|
425
|
+
this.behavior?.setInputValue(value);
|
|
426
|
+
|
|
427
|
+
// Auto-open on input
|
|
428
|
+
if (!this.open && value) {
|
|
429
|
+
this.show();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Handle async loading with debounce
|
|
433
|
+
if (this.loadItems) {
|
|
434
|
+
this.scheduleAsyncLoad(value);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Schedules async loading with debounce.
|
|
440
|
+
*/
|
|
441
|
+
private scheduleAsyncLoad(query: string): void {
|
|
442
|
+
// Clear previous debounce
|
|
443
|
+
if (this.debounceTimeout !== null) {
|
|
444
|
+
window.clearTimeout(this.debounceTimeout);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Cancel previous request
|
|
448
|
+
this.loadAbortController?.abort();
|
|
449
|
+
|
|
450
|
+
this.debounceTimeout = window.setTimeout(() => {
|
|
451
|
+
this.executeAsyncLoad(query);
|
|
452
|
+
}, this.debounce);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Executes the async load.
|
|
457
|
+
*/
|
|
458
|
+
private async executeAsyncLoad(query: string): Promise<void> {
|
|
459
|
+
if (!this.loadItems) return;
|
|
460
|
+
|
|
461
|
+
// Create new abort controller
|
|
462
|
+
this.loadAbortController = new AbortController();
|
|
463
|
+
const signal = this.loadAbortController.signal;
|
|
464
|
+
|
|
465
|
+
this.loading = true;
|
|
466
|
+
this.loadError = null;
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
const results = await this.loadItems(query, signal);
|
|
470
|
+
|
|
471
|
+
// Check if aborted
|
|
472
|
+
if (signal.aborted) return;
|
|
473
|
+
|
|
474
|
+
this.items = results;
|
|
475
|
+
this.filteredItems = results;
|
|
476
|
+
this.loading = false;
|
|
477
|
+
|
|
478
|
+
// Update highlighting
|
|
479
|
+
if (results.length > 0) {
|
|
480
|
+
this.behavior?.highlightFirst();
|
|
481
|
+
this.updateOptionStates();
|
|
482
|
+
}
|
|
483
|
+
} catch (error) {
|
|
484
|
+
// Ignore abort errors
|
|
485
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
this.loading = false;
|
|
490
|
+
this.loadError = error instanceof Error ? error.message : "Failed to load items";
|
|
491
|
+
this.items = [];
|
|
492
|
+
this.filteredItems = [];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Manually trigger async loading (useful for initial load).
|
|
498
|
+
*/
|
|
499
|
+
public async load(query = ""): Promise<void> {
|
|
500
|
+
if (!this.loadItems) return;
|
|
501
|
+
await this.executeAsyncLoad(query);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private handleClick = (event: Event): void => {
|
|
505
|
+
const target = event.target as HTMLElement;
|
|
506
|
+
|
|
507
|
+
// Handle option click
|
|
508
|
+
const option = target.closest("ds-combobox-option") as DsComboboxOption | null;
|
|
509
|
+
if (option && this.contains(option) && !option.disabled && !option.hasAttribute("hidden")) {
|
|
510
|
+
event.preventDefault();
|
|
511
|
+
this.select(option.value);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
private handleKeyDown = (event: KeyboardEvent): void => {
|
|
516
|
+
const target = event.target as HTMLElement;
|
|
517
|
+
const isInput = target.tagName === "INPUT";
|
|
518
|
+
|
|
519
|
+
if (!this.open) {
|
|
520
|
+
// Open on arrow down when focused on input
|
|
521
|
+
if (isInput && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
522
|
+
event.preventDefault();
|
|
523
|
+
this.show();
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
switch (event.key) {
|
|
529
|
+
case "Enter": {
|
|
530
|
+
event.preventDefault();
|
|
531
|
+
const highlightedValue = this.behavior?.state.highlightedValue;
|
|
532
|
+
if (highlightedValue) {
|
|
533
|
+
this.select(highlightedValue);
|
|
534
|
+
} else if (this.creatable && isInput) {
|
|
535
|
+
const inputValue = (target as HTMLInputElement).value.trim();
|
|
536
|
+
if (inputValue) {
|
|
537
|
+
this.create(inputValue);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case "Escape":
|
|
543
|
+
event.preventDefault();
|
|
544
|
+
this.close();
|
|
545
|
+
this.getInputElement()?.focus();
|
|
546
|
+
break;
|
|
547
|
+
case "ArrowDown":
|
|
548
|
+
event.preventDefault();
|
|
549
|
+
this.behavior?.highlightNext();
|
|
550
|
+
this.updateOptionStates();
|
|
551
|
+
this.updateInputAria();
|
|
552
|
+
this.scrollHighlightedIntoView();
|
|
553
|
+
break;
|
|
554
|
+
case "ArrowUp":
|
|
555
|
+
event.preventDefault();
|
|
556
|
+
this.behavior?.highlightPrev();
|
|
557
|
+
this.updateOptionStates();
|
|
558
|
+
this.updateInputAria();
|
|
559
|
+
this.scrollHighlightedIntoView();
|
|
560
|
+
break;
|
|
561
|
+
case "Home":
|
|
562
|
+
if (isInput && (target as HTMLInputElement).selectionStart === 0) {
|
|
563
|
+
event.preventDefault();
|
|
564
|
+
this.behavior?.highlightFirst();
|
|
565
|
+
this.updateOptionStates();
|
|
566
|
+
this.updateInputAria();
|
|
567
|
+
this.scrollHighlightedIntoView();
|
|
568
|
+
}
|
|
569
|
+
break;
|
|
570
|
+
case "End":
|
|
571
|
+
if (
|
|
572
|
+
isInput &&
|
|
573
|
+
(target as HTMLInputElement).selectionEnd === (target as HTMLInputElement).value.length
|
|
574
|
+
) {
|
|
575
|
+
event.preventDefault();
|
|
576
|
+
this.behavior?.highlightLast();
|
|
577
|
+
this.updateOptionStates();
|
|
578
|
+
this.updateInputAria();
|
|
579
|
+
this.scrollHighlightedIntoView();
|
|
580
|
+
}
|
|
581
|
+
break;
|
|
582
|
+
case "Backspace":
|
|
583
|
+
if (this.multiple && isInput && (target as HTMLInputElement).value === "") {
|
|
584
|
+
// Remove last tag
|
|
585
|
+
(this.behavior as ComboboxBehavior<string, true>)?.removeLastTag();
|
|
586
|
+
this.updateOptionStates();
|
|
587
|
+
}
|
|
588
|
+
break;
|
|
589
|
+
case "Tab":
|
|
590
|
+
// Close on tab without preventing default
|
|
591
|
+
this.close();
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
private handleTagRemove = (event: Event): void => {
|
|
597
|
+
const customEvent = event as CustomEvent<{ value: string }>;
|
|
598
|
+
this.removeValue(customEvent.detail.value);
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
private scrollHighlightedIntoView(): void {
|
|
602
|
+
const highlightedValue = this.behavior?.state.highlightedValue;
|
|
603
|
+
if (highlightedValue) {
|
|
604
|
+
const option = this.getOptionByValue(highlightedValue);
|
|
605
|
+
option?.scrollIntoView({ block: "nearest" });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private handleDismiss = (): void => {
|
|
610
|
+
this.close();
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
private setupInputAccessibility(): void {
|
|
614
|
+
const inputWrapper = this.getInputWrapper();
|
|
615
|
+
const content = this.querySelector("ds-combobox-content") as DsComboboxContent | null;
|
|
616
|
+
|
|
617
|
+
if (inputWrapper && content) {
|
|
618
|
+
inputWrapper.disabled = this.disabled;
|
|
619
|
+
inputWrapper.updateAria(this.open, undefined, content.id);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private updateInputAria(): void {
|
|
624
|
+
const inputWrapper = this.getInputWrapper();
|
|
625
|
+
const content = this.querySelector("ds-combobox-content") as DsComboboxContent | null;
|
|
626
|
+
|
|
627
|
+
if (inputWrapper && content) {
|
|
628
|
+
const highlightedValue = this.behavior?.state.highlightedValue;
|
|
629
|
+
const highlightedOption = highlightedValue ? this.getOptionByValue(highlightedValue) : null;
|
|
630
|
+
inputWrapper.updateAria(this.open, highlightedOption?.id, content.id);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private setupPositioning(): void {
|
|
635
|
+
const input = this.getInputElement();
|
|
636
|
+
const content = this.querySelector("ds-combobox-content") as HTMLElement | null;
|
|
637
|
+
|
|
638
|
+
if (!input || !content) return;
|
|
639
|
+
|
|
640
|
+
this.anchorPosition = createAnchorPosition({
|
|
641
|
+
anchor: input,
|
|
642
|
+
floating: content,
|
|
643
|
+
placement: this.placement,
|
|
644
|
+
offset: this.offset,
|
|
645
|
+
flip: this.flip,
|
|
646
|
+
onPositionChange: (pos) => {
|
|
647
|
+
content.setAttribute("data-placement", pos.placement);
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
652
|
+
this.anchorPosition?.update();
|
|
653
|
+
});
|
|
654
|
+
this.resizeObserver.observe(input);
|
|
655
|
+
this.resizeObserver.observe(content);
|
|
656
|
+
|
|
657
|
+
this.scrollHandler = () => {
|
|
658
|
+
this.anchorPosition?.update();
|
|
659
|
+
};
|
|
660
|
+
window.addEventListener("scroll", this.scrollHandler, { passive: true });
|
|
661
|
+
window.addEventListener("resize", this.scrollHandler, { passive: true });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private setupDismissLayer(): void {
|
|
665
|
+
const content = this.querySelector("ds-combobox-content") as HTMLElement | null;
|
|
666
|
+
const input = this.getInputElement();
|
|
667
|
+
|
|
668
|
+
if (!content) return;
|
|
669
|
+
|
|
670
|
+
this.dismissLayer = createDismissableLayer({
|
|
671
|
+
container: content,
|
|
672
|
+
excludeElements: input ? [input] : [],
|
|
673
|
+
onDismiss: this.handleDismiss,
|
|
674
|
+
closeOnEscape: true,
|
|
675
|
+
closeOnOutsideClick: true,
|
|
676
|
+
});
|
|
677
|
+
this.dismissLayer.activate();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private setupRovingFocus(): void {
|
|
681
|
+
const content = this.querySelector("ds-combobox-content") as HTMLElement | null;
|
|
682
|
+
|
|
683
|
+
if (!content) return;
|
|
684
|
+
|
|
685
|
+
this.rovingFocus = createRovingFocus({
|
|
686
|
+
container: content,
|
|
687
|
+
selector: "ds-combobox-option:not([disabled]):not([hidden])",
|
|
688
|
+
direction: "vertical",
|
|
689
|
+
loop: true,
|
|
690
|
+
skipDisabled: true,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private cleanup(): void {
|
|
695
|
+
this.anchorPosition?.destroy();
|
|
696
|
+
this.anchorPosition = null;
|
|
697
|
+
|
|
698
|
+
this.dismissLayer?.deactivate();
|
|
699
|
+
this.dismissLayer = null;
|
|
700
|
+
|
|
701
|
+
this.presence?.destroy();
|
|
702
|
+
this.presence = null;
|
|
703
|
+
|
|
704
|
+
this.rovingFocus?.destroy();
|
|
705
|
+
this.rovingFocus = null;
|
|
706
|
+
|
|
707
|
+
this.virtualizedList?.destroy();
|
|
708
|
+
this.virtualizedList = null;
|
|
709
|
+
|
|
710
|
+
this.resizeObserver?.disconnect();
|
|
711
|
+
this.resizeObserver = null;
|
|
712
|
+
|
|
713
|
+
if (this.scrollHandler) {
|
|
714
|
+
window.removeEventListener("scroll", this.scrollHandler);
|
|
715
|
+
window.removeEventListener("resize", this.scrollHandler);
|
|
716
|
+
this.scrollHandler = null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Cancel any pending async operations
|
|
720
|
+
if (this.debounceTimeout !== null) {
|
|
721
|
+
window.clearTimeout(this.debounceTimeout);
|
|
722
|
+
this.debounceTimeout = null;
|
|
723
|
+
}
|
|
724
|
+
this.loadAbortController?.abort();
|
|
725
|
+
this.loadAbortController = null;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
override async updated(changedProperties: Map<string, unknown>): Promise<void> {
|
|
729
|
+
super.updated(changedProperties);
|
|
730
|
+
|
|
731
|
+
if (changedProperties.has("open")) {
|
|
732
|
+
this.updateInputAria();
|
|
733
|
+
|
|
734
|
+
const content = this.querySelector("ds-combobox-content") as DsComboboxContent | null;
|
|
735
|
+
|
|
736
|
+
if (this.open) {
|
|
737
|
+
this.registerOptions();
|
|
738
|
+
|
|
739
|
+
content?.removeAttribute("hidden");
|
|
740
|
+
|
|
741
|
+
if (content) {
|
|
742
|
+
content.dataState = "open";
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
await this.updateComplete;
|
|
746
|
+
|
|
747
|
+
this.setupPositioning();
|
|
748
|
+
this.setupDismissLayer();
|
|
749
|
+
this.setupRovingFocus();
|
|
750
|
+
|
|
751
|
+
// Highlight first option or current value
|
|
752
|
+
const visibleOptions = this.getEnabledOptions().filter(
|
|
753
|
+
(opt) => !opt.hasAttribute("hidden")
|
|
754
|
+
);
|
|
755
|
+
if (visibleOptions.length > 0) {
|
|
756
|
+
this.behavior?.highlightFirst();
|
|
757
|
+
this.updateOptionStates();
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
if (content) {
|
|
761
|
+
content.dataState = "closed";
|
|
762
|
+
}
|
|
763
|
+
content?.setAttribute("hidden", "");
|
|
764
|
+
|
|
765
|
+
// Clear filter when closing
|
|
766
|
+
for (const option of this.getOptions()) {
|
|
767
|
+
option.removeAttribute("hidden");
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (changedProperties.has("value") || changedProperties.has("values")) {
|
|
773
|
+
this.updateOptionStates();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (changedProperties.has("disabled")) {
|
|
777
|
+
const inputWrapper = this.getInputWrapper();
|
|
778
|
+
if (inputWrapper) {
|
|
779
|
+
inputWrapper.disabled = this.disabled;
|
|
780
|
+
}
|
|
781
|
+
if (this.disabled && this.open) {
|
|
782
|
+
this.close();
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (this.open && (changedProperties.has("placement") || changedProperties.has("offset"))) {
|
|
787
|
+
this.cleanup();
|
|
788
|
+
this.setupPositioning();
|
|
789
|
+
this.setupDismissLayer();
|
|
790
|
+
this.setupRovingFocus();
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Returns true if using data-driven rendering.
|
|
796
|
+
*/
|
|
797
|
+
private get isDataDriven(): boolean {
|
|
798
|
+
return this.items.length > 0 || this.loadItems !== undefined;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Renders a single option from data.
|
|
803
|
+
*/
|
|
804
|
+
private renderDataOption(item: Option<string>) {
|
|
805
|
+
const currentValue = this.multiple
|
|
806
|
+
? ((this.behavior as ComboboxBehavior<string, true>)?.state.value ?? this.values)
|
|
807
|
+
: ([this.behavior?.state.value ?? this.value].filter(Boolean) as string[]);
|
|
808
|
+
const highlightedValue = this.behavior?.state.highlightedValue;
|
|
809
|
+
const isSelected = currentValue.includes(item.value);
|
|
810
|
+
const isHighlighted = item.value === highlightedValue;
|
|
811
|
+
|
|
812
|
+
return html`
|
|
813
|
+
<ds-combobox-option
|
|
814
|
+
value=${item.value}
|
|
815
|
+
?disabled=${item.disabled}
|
|
816
|
+
data-selected=${isSelected || nothing}
|
|
817
|
+
data-highlighted=${isHighlighted || nothing}
|
|
818
|
+
>
|
|
819
|
+
${item.label}
|
|
820
|
+
</ds-combobox-option>
|
|
821
|
+
`;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Renders loading state.
|
|
826
|
+
*/
|
|
827
|
+
private renderLoading() {
|
|
828
|
+
return html`
|
|
829
|
+
<div class="ds-combobox__loading" role="status" aria-live="polite">
|
|
830
|
+
<slot name="loading">Loading...</slot>
|
|
831
|
+
</div>
|
|
832
|
+
`;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Renders error state.
|
|
837
|
+
*/
|
|
838
|
+
private renderError() {
|
|
839
|
+
return html`
|
|
840
|
+
<div class="ds-combobox__error" role="alert">
|
|
841
|
+
<slot name="error">${this.loadError}</slot>
|
|
842
|
+
</div>
|
|
843
|
+
`;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Renders empty state.
|
|
848
|
+
*/
|
|
849
|
+
private renderEmpty() {
|
|
850
|
+
return html`
|
|
851
|
+
<div class="ds-combobox__empty">
|
|
852
|
+
<slot name="empty">No results found</slot>
|
|
853
|
+
</div>
|
|
854
|
+
`;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Form association implementation
|
|
858
|
+
|
|
859
|
+
protected getFormValue(): FormData | string | null {
|
|
860
|
+
if (this.multiple) {
|
|
861
|
+
// For multi-select, submit as FormData with multiple values
|
|
862
|
+
if (this.values.length === 0) return null;
|
|
863
|
+
const formData = new FormData();
|
|
864
|
+
for (const val of this.values) {
|
|
865
|
+
formData.append(this.name, val);
|
|
866
|
+
}
|
|
867
|
+
return formData;
|
|
868
|
+
}
|
|
869
|
+
return this.value || null;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
protected getValidationAnchor(): HTMLElement | undefined {
|
|
873
|
+
return this.getInputElement() as HTMLElement | undefined;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
protected getValidationFlags(): ValidationFlags {
|
|
877
|
+
const hasValue = this.multiple ? this.values.length > 0 : Boolean(this.value);
|
|
878
|
+
if (this.required && !hasValue) {
|
|
879
|
+
return { valueMissing: true };
|
|
880
|
+
}
|
|
881
|
+
return {};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
protected getValidationMessage(flags: ValidationFlags): string {
|
|
885
|
+
if (flags.valueMissing) {
|
|
886
|
+
return "Please select an option";
|
|
887
|
+
}
|
|
888
|
+
return "";
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
protected shouldUpdateFormValue(changedProperties: PropertyValues): boolean {
|
|
892
|
+
return changedProperties.has("value") || changedProperties.has("values");
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
protected shouldUpdateValidity(changedProperties: PropertyValues): boolean {
|
|
896
|
+
return changedProperties.has("value") || changedProperties.has("values");
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
protected onFormReset(): void {
|
|
900
|
+
if (this.multiple) {
|
|
901
|
+
this.values = [...this._defaultValues];
|
|
902
|
+
} else {
|
|
903
|
+
this.value = this._defaultValue;
|
|
904
|
+
}
|
|
905
|
+
this.updateOptionStates();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
protected onFormStateRestore(
|
|
909
|
+
state: string | File | FormData | null,
|
|
910
|
+
_mode: "restore" | "autocomplete"
|
|
911
|
+
): void {
|
|
912
|
+
if (this.multiple && state instanceof FormData) {
|
|
913
|
+
this.values = state.getAll(this.name) as string[];
|
|
914
|
+
} else if (typeof state === "string") {
|
|
915
|
+
this.value = state;
|
|
916
|
+
}
|
|
917
|
+
this.updateOptionStates();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Renders data-driven options.
|
|
922
|
+
*/
|
|
923
|
+
private renderDataOptions() {
|
|
924
|
+
const items = this.filteredItems.length > 0 ? this.filteredItems : this.items;
|
|
925
|
+
|
|
926
|
+
if (this.loading) {
|
|
927
|
+
return this.renderLoading();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (this.loadError) {
|
|
931
|
+
return this.renderError();
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (items.length === 0) {
|
|
935
|
+
return this.renderEmpty();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Use virtualization for large lists
|
|
939
|
+
if (this.virtualize && items.length > this.virtualizationThreshold) {
|
|
940
|
+
return html`
|
|
941
|
+
<div class="ds-combobox__virtualized" style="height: ${Math.min(items.length * 40, 300)}px; overflow-y: auto;">
|
|
942
|
+
${repeat(
|
|
943
|
+
items,
|
|
944
|
+
(item) => item.value,
|
|
945
|
+
(item) => this.renderDataOption(item)
|
|
946
|
+
)}
|
|
947
|
+
</div>
|
|
948
|
+
`;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return repeat(
|
|
952
|
+
items,
|
|
953
|
+
(item) => item.value,
|
|
954
|
+
(item) => this.renderDataOption(item)
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
override render() {
|
|
959
|
+
return html`
|
|
960
|
+
<slot name="tags"></slot>
|
|
961
|
+
<slot name="input"></slot>
|
|
962
|
+
${
|
|
963
|
+
this.isDataDriven
|
|
964
|
+
? html`
|
|
965
|
+
<ds-combobox-content>
|
|
966
|
+
${this.renderDataOptions()}
|
|
967
|
+
</ds-combobox-content>
|
|
968
|
+
`
|
|
969
|
+
: html`<slot></slot>`
|
|
970
|
+
}
|
|
971
|
+
`;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
define("ds-combobox", DsCombobox);
|
|
976
|
+
|
|
977
|
+
declare global {
|
|
978
|
+
interface HTMLElementTagNameMap {
|
|
979
|
+
"ds-combobox": DsCombobox;
|
|
980
|
+
}
|
|
981
|
+
}
|