@hypoth-ui/cli 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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,840 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnchorPosition,
|
|
3
|
+
type DismissableLayer,
|
|
4
|
+
type Option,
|
|
5
|
+
type Placement,
|
|
6
|
+
type Presence,
|
|
7
|
+
type RovingFocus,
|
|
8
|
+
type SelectBehavior,
|
|
9
|
+
type TypeAhead,
|
|
10
|
+
type VirtualizedList,
|
|
11
|
+
createAnchorPosition,
|
|
12
|
+
createDismissableLayer,
|
|
13
|
+
createPresence,
|
|
14
|
+
createRovingFocus,
|
|
15
|
+
createSelectBehavior,
|
|
16
|
+
createTypeAhead,
|
|
17
|
+
prefersReducedMotion,
|
|
18
|
+
} from "@hypoth-ui/primitives-dom";
|
|
19
|
+
import { html, nothing } from "lit";
|
|
20
|
+
import type { PropertyValues } from "lit";
|
|
21
|
+
import { property, state } from "lit/decorators.js";
|
|
22
|
+
import { repeat } from "lit/directives/repeat.js";
|
|
23
|
+
import { DSElement } from "../../base/ds-element.js";
|
|
24
|
+
import { FormAssociatedMixin } from "../../base/form-associated.js";
|
|
25
|
+
import type { ValidationFlags } from "../../base/form-associated.js";
|
|
26
|
+
import { StandardEvents, emitEvent } from "../../events/emit.js";
|
|
27
|
+
import { define } from "../../registry/define.js";
|
|
28
|
+
|
|
29
|
+
// Import child components to ensure they're registered
|
|
30
|
+
import type { DsSelectContent } from "./select-content.js";
|
|
31
|
+
import type { DsSelectOption } from "./select-option.js";
|
|
32
|
+
import type { DsSelectTrigger } from "./select-trigger.js";
|
|
33
|
+
import "./select-content.js";
|
|
34
|
+
import "./select-option.js";
|
|
35
|
+
import "./select-trigger.js";
|
|
36
|
+
import "./select-group.js";
|
|
37
|
+
import "./select-label.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Select component with keyboard navigation, type-ahead, and native form participation.
|
|
41
|
+
*
|
|
42
|
+
* Uses ElementInternals for form association - the selected value is submitted with the form
|
|
43
|
+
* and the select participates in constraint validation.
|
|
44
|
+
*
|
|
45
|
+
* Implements WAI-ARIA Listbox pattern with:
|
|
46
|
+
* - Arrow key navigation between options
|
|
47
|
+
* - Type-ahead search to jump to options
|
|
48
|
+
* - Enter/Space/Click to select options
|
|
49
|
+
* - Escape to close
|
|
50
|
+
*
|
|
51
|
+
* @element ds-select
|
|
52
|
+
*
|
|
53
|
+
* @slot trigger - Trigger element (ds-select-trigger with button inside)
|
|
54
|
+
* @slot - Select content (ds-select-content with ds-select-option children)
|
|
55
|
+
*
|
|
56
|
+
* @fires ds:open-change - Fired when open state changes (detail: { open, reason })
|
|
57
|
+
* @fires ds:change - Fired when value changes (detail: { value, label })
|
|
58
|
+
* @fires ds:invalid - Fired when customValidation is true and validation fails
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```html
|
|
62
|
+
* <form>
|
|
63
|
+
* <ds-select name="fruit" required>
|
|
64
|
+
* <ds-select-trigger slot="trigger">
|
|
65
|
+
* <button>Select fruit</button>
|
|
66
|
+
* </ds-select-trigger>
|
|
67
|
+
* <ds-select-content>
|
|
68
|
+
* <ds-select-option value="apple">Apple</ds-select-option>
|
|
69
|
+
* <ds-select-option value="banana">Banana</ds-select-option>
|
|
70
|
+
* </ds-select-content>
|
|
71
|
+
* </ds-select>
|
|
72
|
+
* <button type="submit">Submit</button>
|
|
73
|
+
* </form>
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export class DsSelect extends FormAssociatedMixin(DSElement) {
|
|
77
|
+
/** Whether the select is open */
|
|
78
|
+
@property({ type: Boolean, reflect: true })
|
|
79
|
+
open = false;
|
|
80
|
+
|
|
81
|
+
/** Current selected value */
|
|
82
|
+
@property({ type: String, reflect: true })
|
|
83
|
+
value = "";
|
|
84
|
+
|
|
85
|
+
/** Placement relative to trigger */
|
|
86
|
+
@property({ type: String, reflect: true })
|
|
87
|
+
placement: Placement = "bottom-start";
|
|
88
|
+
|
|
89
|
+
/** Offset distance from trigger in pixels */
|
|
90
|
+
@property({ type: Number })
|
|
91
|
+
offset = 4;
|
|
92
|
+
|
|
93
|
+
/** Whether to flip placement when near viewport edge */
|
|
94
|
+
@property({ type: Boolean })
|
|
95
|
+
flip = true;
|
|
96
|
+
|
|
97
|
+
/** Whether to animate open/close transitions */
|
|
98
|
+
@property({ type: Boolean })
|
|
99
|
+
animated = true;
|
|
100
|
+
|
|
101
|
+
/** Whether the select is disabled */
|
|
102
|
+
@property({ type: Boolean, reflect: true })
|
|
103
|
+
disabled = false;
|
|
104
|
+
|
|
105
|
+
/** Whether the select is read-only */
|
|
106
|
+
@property({ type: Boolean, reflect: true })
|
|
107
|
+
readonly = false;
|
|
108
|
+
|
|
109
|
+
/** Whether to enable type-ahead search */
|
|
110
|
+
@property({ type: Boolean })
|
|
111
|
+
searchable = true;
|
|
112
|
+
|
|
113
|
+
/** Whether to show clear button */
|
|
114
|
+
@property({ type: Boolean })
|
|
115
|
+
clearable = false;
|
|
116
|
+
|
|
117
|
+
/** Enable virtualization for large lists */
|
|
118
|
+
@property({ type: Boolean })
|
|
119
|
+
virtualize = false;
|
|
120
|
+
|
|
121
|
+
/** Virtualization threshold (default: 100) */
|
|
122
|
+
@property({ type: Number, attribute: "virtualization-threshold" })
|
|
123
|
+
virtualizationThreshold = 100;
|
|
124
|
+
|
|
125
|
+
/** Whether the select is in a loading state (e.g., fetching options) */
|
|
126
|
+
@property({ type: Boolean, reflect: true })
|
|
127
|
+
loading = false;
|
|
128
|
+
|
|
129
|
+
/** Text to display/announce during loading */
|
|
130
|
+
@property({ type: String, attribute: "loading-text" })
|
|
131
|
+
loadingText = "Loading...";
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Data-driven options array. Use this for programmatic option rendering.
|
|
135
|
+
* When provided, options will be rendered from this array instead of slots.
|
|
136
|
+
*/
|
|
137
|
+
@property({ attribute: false })
|
|
138
|
+
items: Option<string>[] = [];
|
|
139
|
+
|
|
140
|
+
/** Visible item IDs for virtualization */
|
|
141
|
+
@state()
|
|
142
|
+
private visibleItemIds = new Set<string>();
|
|
143
|
+
|
|
144
|
+
/** Default value for form reset */
|
|
145
|
+
private _defaultValue = "";
|
|
146
|
+
|
|
147
|
+
private behavior: SelectBehavior<string> | null = null;
|
|
148
|
+
private anchorPosition: AnchorPosition | null = null;
|
|
149
|
+
private dismissLayer: DismissableLayer | null = null;
|
|
150
|
+
private presence: Presence | null = null;
|
|
151
|
+
private rovingFocus: RovingFocus | null = null;
|
|
152
|
+
private typeAhead: TypeAhead | null = null;
|
|
153
|
+
private virtualizedList: VirtualizedList | null = null;
|
|
154
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
155
|
+
private scrollHandler: (() => void) | null = null;
|
|
156
|
+
private focusFirstOnOpen: "first" | "last" | "selected" | null = null;
|
|
157
|
+
|
|
158
|
+
override connectedCallback(): void {
|
|
159
|
+
// Store default value for form reset
|
|
160
|
+
this._defaultValue = this.value;
|
|
161
|
+
|
|
162
|
+
super.connectedCallback();
|
|
163
|
+
|
|
164
|
+
// Initialize behavior
|
|
165
|
+
this.behavior = createSelectBehavior({
|
|
166
|
+
defaultValue: this.value || null,
|
|
167
|
+
disabled: this.disabled,
|
|
168
|
+
readOnly: this.readonly,
|
|
169
|
+
searchable: this.searchable,
|
|
170
|
+
clearable: this.clearable,
|
|
171
|
+
onValueChange: (value) => {
|
|
172
|
+
this.value = value ?? "";
|
|
173
|
+
const option = this.getOptionByValue(value ?? "");
|
|
174
|
+
emitEvent(this, StandardEvents.CHANGE, {
|
|
175
|
+
detail: {
|
|
176
|
+
value: value ?? "",
|
|
177
|
+
label: option?.getLabel() ?? "",
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
onOpenChange: (open) => {
|
|
182
|
+
this.open = open;
|
|
183
|
+
emitEvent(this, StandardEvents.OPEN_CHANGE, {
|
|
184
|
+
detail: { open, reason: "trigger" },
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Listen for trigger interactions
|
|
190
|
+
this.addEventListener("click", this.handleTriggerClick);
|
|
191
|
+
this.addEventListener("keydown", this.handleKeyDown);
|
|
192
|
+
|
|
193
|
+
// Setup after first render
|
|
194
|
+
this.updateComplete.then(() => {
|
|
195
|
+
this.setupTriggerAccessibility();
|
|
196
|
+
this.registerOptions();
|
|
197
|
+
this.updateOptionStates();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
override disconnectedCallback(): void {
|
|
202
|
+
super.disconnectedCallback();
|
|
203
|
+
this.removeEventListener("click", this.handleTriggerClick);
|
|
204
|
+
this.removeEventListener("keydown", this.handleKeyDown);
|
|
205
|
+
this.cleanup();
|
|
206
|
+
this.behavior?.destroy();
|
|
207
|
+
this.behavior = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Opens the select.
|
|
212
|
+
*/
|
|
213
|
+
public show(): void {
|
|
214
|
+
if (this.open || this.disabled || this.loading) return;
|
|
215
|
+
this.behavior?.open();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Closes the select.
|
|
220
|
+
*/
|
|
221
|
+
public close(): void {
|
|
222
|
+
if (!this.open) return;
|
|
223
|
+
|
|
224
|
+
const content = this.querySelector("ds-select-content") as DsSelectContent | null;
|
|
225
|
+
|
|
226
|
+
// If animated, use presence for exit animation
|
|
227
|
+
if (this.animated && content && !prefersReducedMotion()) {
|
|
228
|
+
// Cleanup dismiss layer so it doesn't re-trigger
|
|
229
|
+
this.dismissLayer?.deactivate();
|
|
230
|
+
this.dismissLayer = null;
|
|
231
|
+
|
|
232
|
+
// Create presence for exit animation
|
|
233
|
+
this.presence = createPresence({
|
|
234
|
+
onExitComplete: () => {
|
|
235
|
+
this.completeClose();
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
this.presence.hide(content);
|
|
239
|
+
} else {
|
|
240
|
+
// No animation - close immediately
|
|
241
|
+
this.cleanup();
|
|
242
|
+
this.behavior?.close();
|
|
243
|
+
|
|
244
|
+
// Return focus to trigger
|
|
245
|
+
this.getTriggerElement()?.focus();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Completes the close after exit animation.
|
|
251
|
+
*/
|
|
252
|
+
private completeClose(): void {
|
|
253
|
+
this.cleanup();
|
|
254
|
+
this.behavior?.close();
|
|
255
|
+
|
|
256
|
+
// Return focus to trigger
|
|
257
|
+
this.getTriggerElement()?.focus();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Toggles the select open/closed state.
|
|
262
|
+
*/
|
|
263
|
+
public toggle(): void {
|
|
264
|
+
if (this.open) {
|
|
265
|
+
this.close();
|
|
266
|
+
} else {
|
|
267
|
+
this.show();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Selects a value programmatically.
|
|
273
|
+
*/
|
|
274
|
+
public select(value: string): void {
|
|
275
|
+
if (this.disabled || this.readonly) return;
|
|
276
|
+
this.behavior?.select(value);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Clears the selection.
|
|
281
|
+
*/
|
|
282
|
+
public clear(): void {
|
|
283
|
+
if (!this.clearable || this.disabled || this.readonly) return;
|
|
284
|
+
this.behavior?.clear();
|
|
285
|
+
this.updateOptionStates();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private getTriggerElement(): HTMLElement | null {
|
|
289
|
+
const trigger = this.querySelector("ds-select-trigger") as DsSelectTrigger | null;
|
|
290
|
+
return trigger?.getTriggerElement() ?? null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private getOptions(): DsSelectOption[] {
|
|
294
|
+
const content = this.querySelector("ds-select-content");
|
|
295
|
+
if (!content) return [];
|
|
296
|
+
return Array.from(content.querySelectorAll<DsSelectOption>("ds-select-option"));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private getEnabledOptions(): DsSelectOption[] {
|
|
300
|
+
return this.getOptions().filter((opt) => !opt.disabled);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private getOptionByValue(value: string): DsSelectOption | null {
|
|
304
|
+
return this.getOptions().find((opt) => opt.value === value) ?? null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private registerOptions(): void {
|
|
308
|
+
const options = this.getOptions();
|
|
309
|
+
const items = options.map((opt) => ({
|
|
310
|
+
value: opt.value,
|
|
311
|
+
disabled: opt.disabled,
|
|
312
|
+
}));
|
|
313
|
+
this.behavior?.setItems(items);
|
|
314
|
+
this.behavior?.setOptionCount(options.length);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private updateOptionStates(): void {
|
|
318
|
+
const currentValue = this.behavior?.state.value ?? this.value;
|
|
319
|
+
const highlightedValue = this.behavior?.state.highlightedValue;
|
|
320
|
+
|
|
321
|
+
for (const option of this.getOptions()) {
|
|
322
|
+
option.setSelected(option.value === currentValue);
|
|
323
|
+
option.setHighlighted(option.value === highlightedValue);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private handleTriggerClick = (event: Event): void => {
|
|
328
|
+
const target = event.target as HTMLElement;
|
|
329
|
+
const trigger = target.closest("ds-select-trigger");
|
|
330
|
+
|
|
331
|
+
if (trigger && this.contains(trigger)) {
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
if (this.disabled || this.loading) return;
|
|
334
|
+
this.focusFirstOnOpen = this.value ? "selected" : "first";
|
|
335
|
+
this.toggle();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Handle option click
|
|
339
|
+
const option = target.closest("ds-select-option") as DsSelectOption | null;
|
|
340
|
+
if (option && this.contains(option) && !option.disabled) {
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
this.selectOption(option);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
private handleKeyDown = (event: KeyboardEvent): void => {
|
|
347
|
+
const target = event.target as HTMLElement;
|
|
348
|
+
|
|
349
|
+
// Handle trigger keys when closed
|
|
350
|
+
if (!this.open) {
|
|
351
|
+
const trigger = target.closest("ds-select-trigger");
|
|
352
|
+
if (trigger && this.contains(trigger)) {
|
|
353
|
+
this.handleTriggerKeyDown(event);
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Handle content keys when open
|
|
359
|
+
this.handleContentKeyDown(event);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
private handleTriggerKeyDown(event: KeyboardEvent): void {
|
|
363
|
+
if (this.disabled || this.loading) return;
|
|
364
|
+
|
|
365
|
+
switch (event.key) {
|
|
366
|
+
case "Enter":
|
|
367
|
+
case " ":
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
this.focusFirstOnOpen = this.value ? "selected" : "first";
|
|
370
|
+
this.show();
|
|
371
|
+
break;
|
|
372
|
+
case "ArrowDown":
|
|
373
|
+
event.preventDefault();
|
|
374
|
+
this.focusFirstOnOpen = "first";
|
|
375
|
+
this.show();
|
|
376
|
+
break;
|
|
377
|
+
case "ArrowUp":
|
|
378
|
+
event.preventDefault();
|
|
379
|
+
this.focusFirstOnOpen = "last";
|
|
380
|
+
this.show();
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private handleContentKeyDown(event: KeyboardEvent): void {
|
|
386
|
+
switch (event.key) {
|
|
387
|
+
case "Enter":
|
|
388
|
+
case " ":
|
|
389
|
+
event.preventDefault();
|
|
390
|
+
this.selectHighlighted();
|
|
391
|
+
break;
|
|
392
|
+
case "Escape":
|
|
393
|
+
event.preventDefault();
|
|
394
|
+
this.close();
|
|
395
|
+
break;
|
|
396
|
+
case "ArrowDown":
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
this.behavior?.highlightNext();
|
|
399
|
+
this.updateOptionStates();
|
|
400
|
+
this.updateTriggerAria();
|
|
401
|
+
break;
|
|
402
|
+
case "ArrowUp":
|
|
403
|
+
event.preventDefault();
|
|
404
|
+
this.behavior?.highlightPrev();
|
|
405
|
+
this.updateOptionStates();
|
|
406
|
+
this.updateTriggerAria();
|
|
407
|
+
break;
|
|
408
|
+
case "Home":
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
this.behavior?.highlightFirst();
|
|
411
|
+
this.updateOptionStates();
|
|
412
|
+
this.updateTriggerAria();
|
|
413
|
+
break;
|
|
414
|
+
case "End":
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
this.behavior?.highlightLast();
|
|
417
|
+
this.updateOptionStates();
|
|
418
|
+
this.updateTriggerAria();
|
|
419
|
+
break;
|
|
420
|
+
case "Tab":
|
|
421
|
+
// Close on tab without preventing default
|
|
422
|
+
this.close();
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private selectOption(option: DsSelectOption): void {
|
|
428
|
+
this.behavior?.select(option.value);
|
|
429
|
+
this.updateOptionStates();
|
|
430
|
+
this.close();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private selectHighlighted(): void {
|
|
434
|
+
const highlightedValue = this.behavior?.state.highlightedValue;
|
|
435
|
+
if (highlightedValue) {
|
|
436
|
+
this.behavior?.select(highlightedValue);
|
|
437
|
+
this.updateOptionStates();
|
|
438
|
+
this.close();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private handleDismiss = (): void => {
|
|
443
|
+
this.close();
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
private setupTriggerAccessibility(): void {
|
|
447
|
+
const trigger = this.querySelector("ds-select-trigger") as DsSelectTrigger | null;
|
|
448
|
+
const content = this.querySelector("ds-select-content") as DsSelectContent | null;
|
|
449
|
+
|
|
450
|
+
if (trigger && content) {
|
|
451
|
+
trigger.disabled = this.disabled;
|
|
452
|
+
trigger.updateAria(this.open, undefined, content.id);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Set aria-busy on the trigger element when loading
|
|
456
|
+
this.updateLoadingState();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private updateLoadingState(): void {
|
|
460
|
+
const triggerElement = this.getTriggerElement();
|
|
461
|
+
if (triggerElement) {
|
|
462
|
+
if (this.loading) {
|
|
463
|
+
triggerElement.setAttribute("aria-busy", "true");
|
|
464
|
+
} else {
|
|
465
|
+
triggerElement.removeAttribute("aria-busy");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private updateTriggerAria(): void {
|
|
471
|
+
const trigger = this.querySelector("ds-select-trigger") as DsSelectTrigger | null;
|
|
472
|
+
const content = this.querySelector("ds-select-content") as DsSelectContent | null;
|
|
473
|
+
|
|
474
|
+
if (trigger && content) {
|
|
475
|
+
const highlightedValue = this.behavior?.state.highlightedValue;
|
|
476
|
+
const highlightedOption = highlightedValue ? this.getOptionByValue(highlightedValue) : null;
|
|
477
|
+
trigger.updateAria(this.open, highlightedOption?.id, content.id);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private setupPositioning(): void {
|
|
482
|
+
const trigger = this.getTriggerElement();
|
|
483
|
+
const content = this.querySelector("ds-select-content") as HTMLElement | null;
|
|
484
|
+
|
|
485
|
+
if (!trigger || !content) return;
|
|
486
|
+
|
|
487
|
+
// Setup anchor positioning
|
|
488
|
+
this.anchorPosition = createAnchorPosition({
|
|
489
|
+
anchor: trigger,
|
|
490
|
+
floating: content,
|
|
491
|
+
placement: this.placement,
|
|
492
|
+
offset: this.offset,
|
|
493
|
+
flip: this.flip,
|
|
494
|
+
onPositionChange: (pos) => {
|
|
495
|
+
content.setAttribute("data-placement", pos.placement);
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Setup resize observer for repositioning
|
|
500
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
501
|
+
this.anchorPosition?.update();
|
|
502
|
+
});
|
|
503
|
+
this.resizeObserver.observe(trigger);
|
|
504
|
+
this.resizeObserver.observe(content);
|
|
505
|
+
|
|
506
|
+
// Setup scroll handler for repositioning
|
|
507
|
+
this.scrollHandler = () => {
|
|
508
|
+
this.anchorPosition?.update();
|
|
509
|
+
};
|
|
510
|
+
window.addEventListener("scroll", this.scrollHandler, { passive: true });
|
|
511
|
+
window.addEventListener("resize", this.scrollHandler, { passive: true });
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private setupDismissLayer(): void {
|
|
515
|
+
const content = this.querySelector("ds-select-content") as HTMLElement | null;
|
|
516
|
+
const trigger = this.getTriggerElement();
|
|
517
|
+
|
|
518
|
+
if (!content) return;
|
|
519
|
+
|
|
520
|
+
this.dismissLayer = createDismissableLayer({
|
|
521
|
+
container: content,
|
|
522
|
+
excludeElements: trigger ? [trigger] : [],
|
|
523
|
+
onDismiss: this.handleDismiss,
|
|
524
|
+
closeOnEscape: true,
|
|
525
|
+
closeOnOutsideClick: true,
|
|
526
|
+
});
|
|
527
|
+
this.dismissLayer.activate();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private setupRovingFocus(): void {
|
|
531
|
+
const content = this.querySelector("ds-select-content") as HTMLElement | null;
|
|
532
|
+
|
|
533
|
+
if (!content) return;
|
|
534
|
+
|
|
535
|
+
this.rovingFocus = createRovingFocus({
|
|
536
|
+
container: content,
|
|
537
|
+
selector: "ds-select-option:not([disabled])",
|
|
538
|
+
direction: "vertical",
|
|
539
|
+
loop: true,
|
|
540
|
+
skipDisabled: true,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private setupTypeAhead(): void {
|
|
545
|
+
if (!this.searchable) return;
|
|
546
|
+
|
|
547
|
+
const content = this.querySelector("ds-select-content") as HTMLElement | null;
|
|
548
|
+
if (!content) return;
|
|
549
|
+
|
|
550
|
+
this.typeAhead = createTypeAhead({
|
|
551
|
+
items: () => this.getEnabledOptions() as HTMLElement[],
|
|
552
|
+
getText: (item) => (item as DsSelectOption).getLabel(),
|
|
553
|
+
onMatch: (item) => {
|
|
554
|
+
const option = item as DsSelectOption;
|
|
555
|
+
this.behavior?.highlight(option.value);
|
|
556
|
+
this.updateOptionStates();
|
|
557
|
+
this.updateTriggerAria();
|
|
558
|
+
// Scroll into view
|
|
559
|
+
option.scrollIntoView({ block: "nearest" });
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// Wire type-ahead to keydown events
|
|
564
|
+
content.addEventListener("keydown", this.handleTypeAheadKeyDown);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private handleTypeAheadKeyDown = (event: KeyboardEvent): void => {
|
|
568
|
+
this.typeAhead?.handleKeyDown(event);
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
private focusInitialItem(): void {
|
|
572
|
+
const content = this.querySelector("ds-select-content");
|
|
573
|
+
if (!content) return;
|
|
574
|
+
|
|
575
|
+
const options = this.getEnabledOptions();
|
|
576
|
+
if (options.length === 0) return;
|
|
577
|
+
|
|
578
|
+
let initialIndex = 0;
|
|
579
|
+
|
|
580
|
+
if (this.focusFirstOnOpen === "last") {
|
|
581
|
+
initialIndex = options.length - 1;
|
|
582
|
+
} else if (this.focusFirstOnOpen === "selected" && this.value) {
|
|
583
|
+
const selectedIndex = options.findIndex((opt) => opt.value === this.value);
|
|
584
|
+
if (selectedIndex >= 0) {
|
|
585
|
+
initialIndex = selectedIndex;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Highlight the initial option
|
|
590
|
+
const initialOption = options[initialIndex];
|
|
591
|
+
if (initialOption) {
|
|
592
|
+
this.behavior?.highlight(initialOption.value);
|
|
593
|
+
this.updateOptionStates();
|
|
594
|
+
this.rovingFocus?.setFocusedIndex(initialIndex);
|
|
595
|
+
initialOption.scrollIntoView({ block: "nearest" });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
this.focusFirstOnOpen = null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private cleanup(): void {
|
|
602
|
+
const content = this.querySelector("ds-select-content") as HTMLElement | null;
|
|
603
|
+
|
|
604
|
+
// Cleanup type-ahead listener
|
|
605
|
+
if (content) {
|
|
606
|
+
content.removeEventListener("keydown", this.handleTypeAheadKeyDown);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Cleanup anchor positioning
|
|
610
|
+
this.anchorPosition?.destroy();
|
|
611
|
+
this.anchorPosition = null;
|
|
612
|
+
|
|
613
|
+
// Cleanup dismiss layer
|
|
614
|
+
this.dismissLayer?.deactivate();
|
|
615
|
+
this.dismissLayer = null;
|
|
616
|
+
|
|
617
|
+
// Cleanup presence
|
|
618
|
+
this.presence?.destroy();
|
|
619
|
+
this.presence = null;
|
|
620
|
+
|
|
621
|
+
// Cleanup roving focus
|
|
622
|
+
this.rovingFocus?.destroy();
|
|
623
|
+
this.rovingFocus = null;
|
|
624
|
+
|
|
625
|
+
// Cleanup type-ahead
|
|
626
|
+
this.typeAhead?.reset();
|
|
627
|
+
this.typeAhead = null;
|
|
628
|
+
|
|
629
|
+
// Cleanup virtualized list
|
|
630
|
+
this.virtualizedList?.destroy();
|
|
631
|
+
this.virtualizedList = null;
|
|
632
|
+
|
|
633
|
+
// Cleanup resize observer
|
|
634
|
+
this.resizeObserver?.disconnect();
|
|
635
|
+
this.resizeObserver = null;
|
|
636
|
+
|
|
637
|
+
// Cleanup scroll handler
|
|
638
|
+
if (this.scrollHandler) {
|
|
639
|
+
window.removeEventListener("scroll", this.scrollHandler);
|
|
640
|
+
window.removeEventListener("resize", this.scrollHandler);
|
|
641
|
+
this.scrollHandler = null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
override async updated(changedProperties: Map<string, unknown>): Promise<void> {
|
|
646
|
+
super.updated(changedProperties);
|
|
647
|
+
|
|
648
|
+
if (changedProperties.has("open")) {
|
|
649
|
+
this.updateTriggerAria();
|
|
650
|
+
|
|
651
|
+
const content = this.querySelector("ds-select-content") as DsSelectContent | null;
|
|
652
|
+
|
|
653
|
+
if (this.open) {
|
|
654
|
+
// Re-register options in case they changed
|
|
655
|
+
this.registerOptions();
|
|
656
|
+
|
|
657
|
+
// Show content
|
|
658
|
+
content?.removeAttribute("hidden");
|
|
659
|
+
|
|
660
|
+
// Set data-state to open for entry animation
|
|
661
|
+
if (content) {
|
|
662
|
+
content.dataState = "open";
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Wait for DOM update
|
|
666
|
+
await this.updateComplete;
|
|
667
|
+
|
|
668
|
+
// Setup all behaviors
|
|
669
|
+
this.setupPositioning();
|
|
670
|
+
this.setupDismissLayer();
|
|
671
|
+
this.setupRovingFocus();
|
|
672
|
+
this.setupTypeAhead();
|
|
673
|
+
|
|
674
|
+
// Focus initial item
|
|
675
|
+
this.focusInitialItem();
|
|
676
|
+
} else {
|
|
677
|
+
// Set data-state to closed for exit animation
|
|
678
|
+
if (content) {
|
|
679
|
+
content.dataState = "closed";
|
|
680
|
+
}
|
|
681
|
+
// Hide content
|
|
682
|
+
content?.setAttribute("hidden", "");
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (changedProperties.has("value")) {
|
|
687
|
+
this.updateOptionStates();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (changedProperties.has("disabled")) {
|
|
691
|
+
const trigger = this.querySelector("ds-select-trigger") as DsSelectTrigger | null;
|
|
692
|
+
if (trigger) {
|
|
693
|
+
trigger.disabled = this.disabled;
|
|
694
|
+
}
|
|
695
|
+
if (this.disabled && this.open) {
|
|
696
|
+
this.close();
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Handle loading state changes
|
|
701
|
+
if (changedProperties.has("loading")) {
|
|
702
|
+
this.updateLoadingState();
|
|
703
|
+
// Close dropdown if loading starts while open
|
|
704
|
+
if (this.loading && this.open) {
|
|
705
|
+
this.close();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Update positioning if placement or offset changes while open
|
|
710
|
+
if (this.open && (changedProperties.has("placement") || changedProperties.has("offset"))) {
|
|
711
|
+
this.cleanup();
|
|
712
|
+
this.setupPositioning();
|
|
713
|
+
this.setupDismissLayer();
|
|
714
|
+
this.setupRovingFocus();
|
|
715
|
+
this.setupTypeAhead();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Form association implementation
|
|
720
|
+
|
|
721
|
+
protected getFormValue(): string | null {
|
|
722
|
+
return this.value || null;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
protected getValidationAnchor(): HTMLElement | undefined {
|
|
726
|
+
return this.getTriggerElement() as HTMLElement | undefined;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
protected getValidationFlags(): ValidationFlags {
|
|
730
|
+
if (this.required && !this.value) {
|
|
731
|
+
return { valueMissing: true };
|
|
732
|
+
}
|
|
733
|
+
return {};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
protected getValidationMessage(flags: ValidationFlags): string {
|
|
737
|
+
if (flags.valueMissing) {
|
|
738
|
+
return "Please select an option";
|
|
739
|
+
}
|
|
740
|
+
return "";
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
protected shouldUpdateFormValue(changedProperties: PropertyValues): boolean {
|
|
744
|
+
return changedProperties.has("value");
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
protected shouldUpdateValidity(changedProperties: PropertyValues): boolean {
|
|
748
|
+
return changedProperties.has("value");
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
protected onFormReset(): void {
|
|
752
|
+
this.value = this._defaultValue;
|
|
753
|
+
this.updateOptionStates();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
protected onFormStateRestore(
|
|
757
|
+
state: string | File | FormData | null,
|
|
758
|
+
_mode: "restore" | "autocomplete"
|
|
759
|
+
): void {
|
|
760
|
+
if (typeof state === "string") {
|
|
761
|
+
this.value = state;
|
|
762
|
+
this.updateOptionStates();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Returns true if using data-driven rendering.
|
|
768
|
+
*/
|
|
769
|
+
private get isDataDriven(): boolean {
|
|
770
|
+
return this.items.length > 0;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Renders a single option from data.
|
|
775
|
+
*/
|
|
776
|
+
private renderDataOption(item: Option<string>) {
|
|
777
|
+
const currentValue = this.behavior?.state.value ?? this.value;
|
|
778
|
+
const highlightedValue = this.behavior?.state.highlightedValue;
|
|
779
|
+
const isSelected = item.value === currentValue;
|
|
780
|
+
const isHighlighted = item.value === highlightedValue;
|
|
781
|
+
|
|
782
|
+
return html`
|
|
783
|
+
<ds-select-option
|
|
784
|
+
value=${item.value}
|
|
785
|
+
?disabled=${item.disabled}
|
|
786
|
+
data-selected=${isSelected || nothing}
|
|
787
|
+
data-highlighted=${isHighlighted || nothing}
|
|
788
|
+
>
|
|
789
|
+
${item.label}
|
|
790
|
+
</ds-select-option>
|
|
791
|
+
`;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Renders data-driven options with optional virtualization.
|
|
796
|
+
*/
|
|
797
|
+
private renderDataOptions() {
|
|
798
|
+
// Use virtualization for large lists
|
|
799
|
+
if (this.virtualize && this.items.length > this.virtualizationThreshold) {
|
|
800
|
+
return html`
|
|
801
|
+
<div class="ds-select__virtualized" style="height: ${Math.min(this.items.length * 40, 300)}px; overflow-y: auto;">
|
|
802
|
+
${repeat(
|
|
803
|
+
this.items,
|
|
804
|
+
(item) => item.value,
|
|
805
|
+
(item) => this.renderDataOption(item)
|
|
806
|
+
)}
|
|
807
|
+
</div>
|
|
808
|
+
`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return repeat(
|
|
812
|
+
this.items,
|
|
813
|
+
(item) => item.value,
|
|
814
|
+
(item) => this.renderDataOption(item)
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
override render() {
|
|
819
|
+
return html`
|
|
820
|
+
<slot name="trigger"></slot>
|
|
821
|
+
${
|
|
822
|
+
this.isDataDriven
|
|
823
|
+
? html`
|
|
824
|
+
<ds-select-content>
|
|
825
|
+
${this.renderDataOptions()}
|
|
826
|
+
</ds-select-content>
|
|
827
|
+
`
|
|
828
|
+
: html`<slot></slot>`
|
|
829
|
+
}
|
|
830
|
+
`;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
define("ds-select", DsSelect);
|
|
835
|
+
|
|
836
|
+
declare global {
|
|
837
|
+
interface HTMLElementTagNameMap {
|
|
838
|
+
"ds-select": DsSelect;
|
|
839
|
+
}
|
|
840
|
+
}
|