@ui-sdk/web 0.0.0-stage → 0.1.0-beta.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 +202 -0
- package/README.md +10 -2
- package/browser/web/accordion.js +1 -0
- package/browser/web/alert-dialog.js +1 -0
- package/browser/web/autocomplete.js +1 -0
- package/browser/web/avatar.js +1 -0
- package/browser/web/button.js +1 -0
- package/browser/web/calendar.js +1 -0
- package/browser/web/carousel.js +1 -0
- package/browser/web/checkbox.js +1 -0
- package/browser/web/chunks/0eRGibL6.js +1 -0
- package/browser/web/chunks/2bIEUO6_.js +1 -0
- package/browser/web/chunks/5A9w2ULz.js +1 -0
- package/browser/web/chunks/5RSyydjV.js +1 -0
- package/browser/web/chunks/9pWTy2wl.js +1 -0
- package/browser/web/chunks/B0f2j6zz.js +1 -0
- package/browser/web/chunks/B1rux6br.js +1 -0
- package/browser/web/chunks/B3-APaNf.js +1 -0
- package/browser/web/chunks/B7EgSgjQ.js +1 -0
- package/browser/web/chunks/B8QcOzJU.js +1 -0
- package/browser/web/chunks/BFueEkwm.js +3 -0
- package/browser/web/chunks/BJj3oS29.js +1 -0
- package/browser/web/chunks/BNSvTCpr.js +1 -0
- package/browser/web/chunks/BPD0kAZB.js +1 -0
- package/browser/web/chunks/BQeduZqT.js +1 -0
- package/browser/web/chunks/B_CVFyiz.js +1 -0
- package/browser/web/chunks/B_ns1YHO.js +1 -0
- package/browser/web/chunks/BcwkVTsR.js +1 -0
- package/browser/web/chunks/BeaQ6TYM.js +1 -0
- package/browser/web/chunks/BgBB013u.js +1 -0
- package/browser/web/chunks/BgsKhHDm.js +1 -0
- package/browser/web/chunks/BjMcbuhi.js +9 -0
- package/browser/web/chunks/Bjiy_F2N.js +1 -0
- package/browser/web/chunks/BqScCLwv.js +1 -0
- package/browser/web/chunks/BqX3Utqx.js +1 -0
- package/browser/web/chunks/BtVkQf3z.js +1 -0
- package/browser/web/chunks/By30h-7B.js +1 -0
- package/browser/web/chunks/C1zqYpy2.js +1 -0
- package/browser/web/chunks/C436juFf.js +1 -0
- package/browser/web/chunks/C4_bULs1.js +1 -0
- package/browser/web/chunks/C4omqJEz.js +1 -0
- package/browser/web/chunks/C7tt0T3n.js +1 -0
- package/browser/web/chunks/C8w7s5dg.js +1 -0
- package/browser/web/chunks/CElY4erO.js +1 -0
- package/browser/web/chunks/CFODo-m2.js +1 -0
- package/browser/web/chunks/CL4wjam9.js +1 -0
- package/browser/web/chunks/CMg-8UVf.js +1 -0
- package/browser/web/chunks/COWKfakj.js +1 -0
- package/browser/web/chunks/CQBU0qZ4.js +1 -0
- package/browser/web/chunks/CV6uABgc.js +1 -0
- package/browser/web/chunks/C_SWFpWQ.js +1 -0
- package/browser/web/chunks/C_Uanbrl.js +1 -0
- package/browser/web/chunks/Ce_CSanC.js +1 -0
- package/browser/web/chunks/Cg-ZmRhS.js +1 -0
- package/browser/web/chunks/Cgf2wPI_.js +1 -0
- package/browser/web/chunks/Cuep_3Vf.js +1 -0
- package/browser/web/chunks/D02PaHn6.js +1 -0
- package/browser/web/chunks/D5LiaI5R.js +1 -0
- package/browser/web/chunks/D9UgVYvF.js +1 -0
- package/browser/web/chunks/DC08tnkE.js +2 -0
- package/browser/web/chunks/DG8YGSvu.js +1 -0
- package/browser/web/chunks/DIQd0FYD.js +1 -0
- package/browser/web/chunks/DMLqacE8.js +1 -0
- package/browser/web/chunks/DSUI3yI-.js +1 -0
- package/browser/web/chunks/DT3Hb76X.js +1 -0
- package/browser/web/chunks/DY75g3-W.js +1 -0
- package/browser/web/chunks/DZpNQKZ9.js +1 -0
- package/browser/web/chunks/Db-W1wrC.js +1 -0
- package/browser/web/chunks/Dc1xK3Ii.js +1 -0
- package/browser/web/chunks/DdF6awFs.js +1 -0
- package/browser/web/chunks/DdaCLml8.js +1 -0
- package/browser/web/chunks/Dh8MLjQO.js +1 -0
- package/browser/web/chunks/DiuslJRy.js +1 -0
- package/browser/web/chunks/DjRcw3Y0.js +1 -0
- package/browser/web/chunks/Djdv-16J.js +1 -0
- package/browser/web/chunks/DnFXe_lF.js +1 -0
- package/browser/web/chunks/DneGv1oC.js +1 -0
- package/browser/web/chunks/DsVxP-p-.js +1 -0
- package/browser/web/chunks/DwDilCo2.js +1 -0
- package/browser/web/chunks/DwV48AXv.js +1 -0
- package/browser/web/chunks/F37aF4-L.js +1 -0
- package/browser/web/chunks/LyB6XIbN.js +1 -0
- package/browser/web/chunks/NKnbYGjC.js +1 -0
- package/browser/web/chunks/Vk-rVi16.js +1 -0
- package/browser/web/chunks/XsM0J1Ex.js +1 -0
- package/browser/web/chunks/asSN9iEp.js +1 -0
- package/browser/web/chunks/c-AjHfdF.js +1 -0
- package/browser/web/chunks/dR-12TPY.js +1 -0
- package/browser/web/chunks/hR4xM0EC.js +1 -0
- package/browser/web/chunks/hqOaBxAg.js +1 -0
- package/browser/web/chunks/kMKqqeG4.js +1 -0
- package/browser/web/chunks/kXboBcfi.js +1 -0
- package/browser/web/chunks/l4aQIg22.js +1 -0
- package/browser/web/chunks/tJC0JSU4.js +1 -0
- package/browser/web/chunks/wHdS_QYM.js +1 -0
- package/browser/web/chunks/yv8dTvNI.js +1 -0
- package/browser/web/chunks/yw9M3nfi.js +1 -0
- package/browser/web/collapsible.js +1 -0
- package/browser/web/color-picker.js +1 -0
- package/browser/web/combobox.js +2 -0
- package/browser/web/command.js +1 -0
- package/browser/web/core.js +5 -0
- package/browser/web/date-input.js +1 -0
- package/browser/web/date-picker.js +1 -0
- package/browser/web/dialog.js +1 -0
- package/browser/web/drawer.js +1 -0
- package/browser/web/field.js +1 -0
- package/browser/web/file-input.js +1 -0
- package/browser/web/hover-card.js +1 -0
- package/browser/web/image-cropper.js +1 -0
- package/browser/web/input.js +1 -0
- package/browser/web/label.js +1 -0
- package/browser/web/listbox.js +1 -0
- package/browser/web/menu.js +1 -0
- package/browser/web/menubar.js +1 -0
- package/browser/web/meter.js +1 -0
- package/browser/web/navigation-menu.js +1 -0
- package/browser/web/number-input.js +1 -0
- package/browser/web/otp-input.js +2 -0
- package/browser/web/overlays.js +1 -0
- package/browser/web/pagination.js +1 -0
- package/browser/web/password-input.js +1 -0
- package/browser/web/popover.js +1 -0
- package/browser/web/preferences.js +1 -0
- package/browser/web/progress.js +1 -0
- package/browser/web/radio.js +1 -0
- package/browser/web/rating.js +1 -0
- package/browser/web/resizable.js +1 -0
- package/browser/web/scroll-area.js +1 -0
- package/browser/web/segmented-control.js +1 -0
- package/browser/web/select.js +1 -0
- package/browser/web/separator.js +1 -0
- package/browser/web/slider.js +1 -0
- package/browser/web/switch.js +1 -0
- package/browser/web/tabs.js +1 -0
- package/browser/web/textarea.js +1 -0
- package/browser/web/time-input.js +1 -0
- package/browser/web/timeline.js +1 -0
- package/browser/web/toast.js +1 -0
- package/browser/web/toggle.js +1 -0
- package/browser/web/token-input.js +8 -0
- package/browser/web/toolbar.js +1 -0
- package/browser/web/tooltip.js +1 -0
- package/browser/web/tree.js +1 -0
- package/browser/web/trigger-menu.js +4 -0
- package/browser/web/waterfall.js +1 -0
- package/browser/web.css +3378 -0
- package/browser/web.js +3 -0
- package/custom-elements.json +32315 -0
- package/dist/accordion.d.ts +2 -0
- package/dist/accordion.js +57 -0
- package/dist/action.d.ts +16 -0
- package/dist/action.js +39 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +7 -0
- package/dist/autocomplete.d.ts +2 -0
- package/dist/autocomplete.js +120 -0
- package/dist/autoload.d.ts +18 -0
- package/dist/autoload.js +63 -0
- package/dist/avatar.d.ts +2 -0
- package/dist/avatar.js +27 -0
- package/dist/browser-core.d.ts +2 -0
- package/dist/browser-core.js +8 -0
- package/dist/button.d.ts +2 -0
- package/dist/button.js +35 -0
- package/dist/calendar.d.ts +23 -0
- package/dist/calendar.js +429 -0
- package/dist/carousel.d.ts +2 -0
- package/dist/carousel.js +54 -0
- package/dist/checkbox.d.ts +4 -0
- package/dist/checkbox.js +75 -0
- package/dist/collapsible.d.ts +2 -0
- package/dist/collapsible.js +28 -0
- package/dist/color-picker.d.ts +2 -0
- package/dist/color-picker.js +64 -0
- package/dist/combobox.d.ts +2 -0
- package/dist/combobox.js +334 -0
- package/dist/command.d.ts +2 -0
- package/dist/command.js +122 -0
- package/dist/date-input.d.ts +3 -0
- package/dist/date-input.js +87 -0
- package/dist/date-picker.d.ts +2 -0
- package/dist/date-picker.js +174 -0
- package/dist/default-text.d.ts +3 -0
- package/dist/default-text.js +24 -0
- package/dist/define-element.d.ts +404 -0
- package/dist/define-element.js +1489 -0
- package/dist/dialog.d.ts +3 -0
- package/dist/dialog.js +94 -0
- package/dist/drawer.d.ts +2 -0
- package/dist/drawer.js +96 -0
- package/dist/entries/accordion.d.ts +3 -0
- package/dist/entries/accordion.js +7 -0
- package/dist/entries/alert-dialog.d.ts +3 -0
- package/dist/entries/alert-dialog.js +13 -0
- package/dist/entries/autocomplete.d.ts +3 -0
- package/dist/entries/autocomplete.js +7 -0
- package/dist/entries/avatar.d.ts +2 -0
- package/dist/entries/avatar.js +3 -0
- package/dist/entries/button.d.ts +2 -0
- package/dist/entries/button.js +3 -0
- package/dist/entries/calendar.d.ts +3 -0
- package/dist/entries/calendar.js +7 -0
- package/dist/entries/carousel.d.ts +3 -0
- package/dist/entries/carousel.js +7 -0
- package/dist/entries/checkbox.d.ts +3 -0
- package/dist/entries/checkbox.js +15 -0
- package/dist/entries/collapsible.d.ts +3 -0
- package/dist/entries/collapsible.js +7 -0
- package/dist/entries/color-picker.d.ts +3 -0
- package/dist/entries/color-picker.js +7 -0
- package/dist/entries/combobox.d.ts +3 -0
- package/dist/entries/combobox.js +7 -0
- package/dist/entries/command.d.ts +3 -0
- package/dist/entries/command.js +7 -0
- package/dist/entries/date-input.d.ts +3 -0
- package/dist/entries/date-input.js +7 -0
- package/dist/entries/date-picker.d.ts +3 -0
- package/dist/entries/date-picker.js +7 -0
- package/dist/entries/dialog.d.ts +3 -0
- package/dist/entries/dialog.js +7 -0
- package/dist/entries/drawer.d.ts +3 -0
- package/dist/entries/drawer.js +8 -0
- package/dist/entries/field.d.ts +2 -0
- package/dist/entries/field.js +3 -0
- package/dist/entries/file-input.d.ts +3 -0
- package/dist/entries/file-input.js +13 -0
- package/dist/entries/hover-card.d.ts +3 -0
- package/dist/entries/hover-card.js +7 -0
- package/dist/entries/image-cropper.d.ts +3 -0
- package/dist/entries/image-cropper.js +7 -0
- package/dist/entries/input.d.ts +3 -0
- package/dist/entries/input.js +8 -0
- package/dist/entries/label.d.ts +2 -0
- package/dist/entries/label.js +3 -0
- package/dist/entries/listbox.d.ts +3 -0
- package/dist/entries/listbox.js +7 -0
- package/dist/entries/menu.d.ts +3 -0
- package/dist/entries/menu.js +7 -0
- package/dist/entries/menubar.d.ts +3 -0
- package/dist/entries/menubar.js +7 -0
- package/dist/entries/meter.d.ts +2 -0
- package/dist/entries/meter.js +3 -0
- package/dist/entries/navigation-menu.d.ts +3 -0
- package/dist/entries/navigation-menu.js +7 -0
- package/dist/entries/number-input.d.ts +3 -0
- package/dist/entries/number-input.js +7 -0
- package/dist/entries/otp-input.d.ts +3 -0
- package/dist/entries/otp-input.js +7 -0
- package/dist/entries/overlays.d.ts +2 -0
- package/dist/entries/overlays.js +3 -0
- package/dist/entries/pagination.d.ts +3 -0
- package/dist/entries/pagination.js +11 -0
- package/dist/entries/password-input.d.ts +3 -0
- package/dist/entries/password-input.js +7 -0
- package/dist/entries/popover.d.ts +3 -0
- package/dist/entries/popover.js +7 -0
- package/dist/entries/preferences.d.ts +3 -0
- package/dist/entries/preferences.js +13 -0
- package/dist/entries/progress.d.ts +2 -0
- package/dist/entries/progress.js +3 -0
- package/dist/entries/radio.d.ts +3 -0
- package/dist/entries/radio.js +7 -0
- package/dist/entries/rating.d.ts +3 -0
- package/dist/entries/rating.js +8 -0
- package/dist/entries/resizable.d.ts +3 -0
- package/dist/entries/resizable.js +11 -0
- package/dist/entries/scroll-area.d.ts +3 -0
- package/dist/entries/scroll-area.js +7 -0
- package/dist/entries/segmented-control.d.ts +3 -0
- package/dist/entries/segmented-control.js +7 -0
- package/dist/entries/select.d.ts +3 -0
- package/dist/entries/select.js +7 -0
- package/dist/entries/separator.d.ts +2 -0
- package/dist/entries/separator.js +3 -0
- package/dist/entries/slider.d.ts +3 -0
- package/dist/entries/slider.js +7 -0
- package/dist/entries/switch.d.ts +3 -0
- package/dist/entries/switch.js +7 -0
- package/dist/entries/tabs.d.ts +3 -0
- package/dist/entries/tabs.js +7 -0
- package/dist/entries/textarea.d.ts +3 -0
- package/dist/entries/textarea.js +7 -0
- package/dist/entries/time-input.d.ts +3 -0
- package/dist/entries/time-input.js +7 -0
- package/dist/entries/timeline.d.ts +3 -0
- package/dist/entries/timeline.js +7 -0
- package/dist/entries/toast.d.ts +2 -0
- package/dist/entries/toast.js +3 -0
- package/dist/entries/toggle.d.ts +3 -0
- package/dist/entries/toggle.js +12 -0
- package/dist/entries/token-input.d.ts +3 -0
- package/dist/entries/token-input.js +17 -0
- package/dist/entries/toolbar.d.ts +3 -0
- package/dist/entries/toolbar.js +7 -0
- package/dist/entries/tooltip.d.ts +3 -0
- package/dist/entries/tooltip.js +7 -0
- package/dist/entries/tree.d.ts +3 -0
- package/dist/entries/tree.js +17 -0
- package/dist/entries/trigger-menu.d.ts +3 -0
- package/dist/entries/trigger-menu.js +11 -0
- package/dist/entries/waterfall.d.ts +3 -0
- package/dist/entries/waterfall.js +11 -0
- package/dist/events.d.ts +1658 -0
- package/dist/events.js +0 -0
- package/dist/field.d.ts +2 -0
- package/dist/field.js +101 -0
- package/dist/file-input.d.ts +2 -0
- package/dist/file-input.js +140 -0
- package/dist/forward-attributes.d.ts +2 -0
- package/dist/forward-attributes.js +22 -0
- package/dist/history.d.ts +2 -0
- package/dist/history.js +11 -0
- package/dist/hover-card.d.ts +2 -0
- package/dist/hover-card.js +31 -0
- package/dist/image-cropper.d.ts +2 -0
- package/dist/image-cropper.js +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +269 -0
- package/dist/input.d.ts +3 -0
- package/dist/input.js +69 -0
- package/dist/label.d.ts +2 -0
- package/dist/label.js +11 -0
- package/dist/list.d.ts +40 -0
- package/dist/list.js +192 -0
- package/dist/listbox.d.ts +2 -0
- package/dist/listbox.js +84 -0
- package/dist/menu.d.ts +4 -0
- package/dist/menu.js +295 -0
- package/dist/menubar.d.ts +4 -0
- package/dist/menubar.js +30 -0
- package/dist/meter.d.ts +2 -0
- package/dist/meter.js +22 -0
- package/dist/navigation-menu.d.ts +2 -0
- package/dist/navigation-menu.js +114 -0
- package/dist/number-input.d.ts +2 -0
- package/dist/number-input.js +73 -0
- package/dist/observe.d.ts +2 -0
- package/dist/observe.js +7 -0
- package/dist/otp-input.d.ts +2 -0
- package/dist/otp-input.js +72 -0
- package/dist/overlays.d.ts +13 -0
- package/dist/overlays.js +156 -0
- package/dist/pagination.d.ts +2 -0
- package/dist/pagination.js +127 -0
- package/dist/password-input.d.ts +2 -0
- package/dist/password-input.js +57 -0
- package/dist/popover.d.ts +2 -0
- package/dist/popover.js +64 -0
- package/dist/popup.d.ts +40 -0
- package/dist/popup.js +112 -0
- package/dist/preferences.d.ts +2 -0
- package/dist/preferences.js +56 -0
- package/dist/progress.d.ts +2 -0
- package/dist/progress.js +22 -0
- package/dist/radio.d.ts +2 -0
- package/dist/radio.js +33 -0
- package/dist/rating.d.ts +2 -0
- package/dist/rating.js +64 -0
- package/dist/resizable.d.ts +2 -0
- package/dist/resizable.js +42 -0
- package/dist/scroll-area.d.ts +2 -0
- package/dist/scroll-area.js +45 -0
- package/dist/segmented-control.d.ts +2 -0
- package/dist/segmented-control.js +28 -0
- package/dist/select.d.ts +2 -0
- package/dist/select.js +239 -0
- package/dist/separator.d.ts +2 -0
- package/dist/separator.js +11 -0
- package/dist/slider.d.ts +2 -0
- package/dist/slider.js +77 -0
- package/dist/tabs.d.ts +2 -0
- package/dist/tabs.js +37 -0
- package/dist/timeline.d.ts +2 -0
- package/dist/timeline.js +27 -0
- package/dist/toast.d.ts +20 -0
- package/dist/toast.js +177 -0
- package/dist/toggle.d.ts +3 -0
- package/dist/toggle.js +64 -0
- package/dist/token-input.d.ts +2 -0
- package/dist/token-input.js +103 -0
- package/dist/toolbar.d.ts +2 -0
- package/dist/toolbar.js +36 -0
- package/dist/tooltip.d.ts +2 -0
- package/dist/tooltip.js +35 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.js +147 -0
- package/dist/trigger-menu.d.ts +2 -0
- package/dist/trigger-menu.js +133 -0
- package/dist/waterfall.d.ts +2 -0
- package/dist/waterfall.js +140 -0
- package/docs/README.md +89 -0
- package/docs/agents.md +1523 -0
- package/docs/components/accordion.md +1401 -0
- package/docs/components/alert-dialog.md +788 -0
- package/docs/components/autocomplete.md +1692 -0
- package/docs/components/avatar.md +261 -0
- package/docs/components/button.md +441 -0
- package/docs/components/calendar.md +1536 -0
- package/docs/components/carousel.md +1055 -0
- package/docs/components/checkbox.md +1045 -0
- package/docs/components/collapsible.md +535 -0
- package/docs/components/color-picker.md +651 -0
- package/docs/components/combobox.md +3274 -0
- package/docs/components/command.md +1431 -0
- package/docs/components/date-input.md +563 -0
- package/docs/components/date-picker.md +1136 -0
- package/docs/components/dialog.md +1360 -0
- package/docs/components/drawer.md +1201 -0
- package/docs/components/field.md +519 -0
- package/docs/components/file-input.md +617 -0
- package/docs/components/hover-card.md +486 -0
- package/docs/components/image-cropper.md +489 -0
- package/docs/components/input.md +327 -0
- package/docs/components/label.md +208 -0
- package/docs/components/listbox.md +1015 -0
- package/docs/components/menu.md +1212 -0
- package/docs/components/menubar.md +460 -0
- package/docs/components/meter.md +298 -0
- package/docs/components/navigation-menu.md +1573 -0
- package/docs/components/number-input.md +1087 -0
- package/docs/components/otp-input.md +558 -0
- package/docs/components/pagination.md +639 -0
- package/docs/components/password-input.md +649 -0
- package/docs/components/popover.md +1433 -0
- package/docs/components/preferences.md +243 -0
- package/docs/components/progress.md +406 -0
- package/docs/components/radio.md +855 -0
- package/docs/components/rating.md +431 -0
- package/docs/components/resizable.md +866 -0
- package/docs/components/scroll-area.md +898 -0
- package/docs/components/segmented-control.md +421 -0
- package/docs/components/select.md +1957 -0
- package/docs/components/separator.md +238 -0
- package/docs/components/slider.md +1018 -0
- package/docs/components/switch.md +583 -0
- package/docs/components/tabs.md +948 -0
- package/docs/components/textarea.md +331 -0
- package/docs/components/time-input.md +235 -0
- package/docs/components/timeline.md +622 -0
- package/docs/components/toast.md +1223 -0
- package/docs/components/toggle.md +522 -0
- package/docs/components/token-input.md +854 -0
- package/docs/components/toolbar.md +815 -0
- package/docs/components/tooltip.md +860 -0
- package/docs/components/tree.md +1243 -0
- package/docs/components/trigger-menu.md +1110 -0
- package/docs/components/waterfall.md +709 -0
- package/docs/controllers.md +66 -0
- package/docs/core.md +8027 -0
- package/docs/custom-parts.md +91 -0
- package/docs/first-component.md +88 -0
- package/docs/forms.md +125 -0
- package/docs/installation.md +141 -0
- package/docs/introduction.md +32 -0
- package/docs/lists-and-menus.md +166 -0
- package/docs/llms.txt +1566 -0
- package/docs/parts-and-composition.md +57 -0
- package/docs/popups.md +92 -0
- package/docs/rendering.md +71 -0
- package/docs/server-rendering.md +118 -0
- package/docs/state-and-events.md +134 -0
- package/docs/styling-and-animation.md +104 -0
- package/docs/utilities/async-options.md +332 -0
- package/docs/utilities/shortcuts.md +184 -0
- package/html-custom-data.json +14792 -0
- package/package.json +302 -3
|
@@ -0,0 +1,3274 @@
|
|
|
1
|
+
# Combobox
|
|
2
|
+
|
|
3
|
+
Search a list of options and choose one or several values.
|
|
4
|
+
|
|
5
|
+
Use a Combobox when people need to find a choice by typing. Start with an input and a list, then add chips, server results, or the option to create something new as you need them.
|
|
6
|
+
|
|
7
|
+
```ts title="demo.ts"
|
|
8
|
+
import "@ui-sdk/web/combobox"
|
|
9
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
10
|
+
|
|
11
|
+
const frameworks = [
|
|
12
|
+
{ value: "next", label: "Next.js" },
|
|
13
|
+
{ value: "sveltekit", label: "SvelteKit" },
|
|
14
|
+
{ value: "nuxt", label: "Nuxt" },
|
|
15
|
+
{ value: "remix", label: "Remix" },
|
|
16
|
+
{ value: "astro", label: "Astro" },
|
|
17
|
+
{ value: "solidstart", label: "SolidStart" },
|
|
18
|
+
{ value: "qwik", label: "Qwik City" },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
function mount(root: HTMLElement) {
|
|
22
|
+
root.innerHTML = `
|
|
23
|
+
<ui-combobox>
|
|
24
|
+
<div data-slot="stack">
|
|
25
|
+
<ui-combobox-label>Framework</ui-combobox-label>
|
|
26
|
+
<div data-slot="row">
|
|
27
|
+
<ui-combobox-input>
|
|
28
|
+
<input placeholder="Select a framework" />
|
|
29
|
+
</ui-combobox-input>
|
|
30
|
+
<ui-combobox-trigger>
|
|
31
|
+
<button aria-label="Show frameworks">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
32
|
+
</ui-combobox-trigger>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<ui-combobox-content hidden>
|
|
36
|
+
<ui-combobox-empty>No frameworks found.</ui-combobox-empty>
|
|
37
|
+
<ui-combobox-list>
|
|
38
|
+
${frameworks
|
|
39
|
+
.map(
|
|
40
|
+
(framework) => `
|
|
41
|
+
<ui-combobox-item value="${framework.value}" label="${framework.label}">
|
|
42
|
+
${framework.label}
|
|
43
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
44
|
+
</ui-combobox-item>
|
|
45
|
+
`
|
|
46
|
+
)
|
|
47
|
+
.join("")}
|
|
48
|
+
</ui-combobox-list>
|
|
49
|
+
</ui-combobox-content>
|
|
50
|
+
</ui-combobox>
|
|
51
|
+
`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { mount }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Type “sv” to find SvelteKit. Press Down Arrow to highlight it, then Enter to choose it.
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install @ui-sdk/web @ui-sdk/core
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Import the element definitions once in your application:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import "@ui-sdk/web/combobox"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Composition
|
|
72
|
+
|
|
73
|
+
Use the following composition to build a `ui-combobox`:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
ui-combobox
|
|
77
|
+
├── ui-combobox-label
|
|
78
|
+
├── ui-combobox-input
|
|
79
|
+
│ └── input
|
|
80
|
+
└── ui-combobox-content
|
|
81
|
+
└── ui-combobox-list
|
|
82
|
+
├── ui-combobox-item
|
|
83
|
+
└── ui-combobox-item
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Name the input, give each option a unique value, and put the options in a list inside the popup. `ui-combobox-input` wraps a native input. The label names the input; choosing an option puts its label in the input.
|
|
87
|
+
|
|
88
|
+
## Usage
|
|
89
|
+
|
|
90
|
+
Import the parts and put them together:
|
|
91
|
+
|
|
92
|
+
```html
|
|
93
|
+
<script type="module">
|
|
94
|
+
import "@ui-sdk/web/combobox"
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<ui-combobox>
|
|
98
|
+
<ui-combobox-label>Fruit</ui-combobox-label>
|
|
99
|
+
<ui-combobox-input><input placeholder="Search fruits…" /></ui-combobox-input>
|
|
100
|
+
<ui-combobox-content hidden>
|
|
101
|
+
<ui-combobox-list>
|
|
102
|
+
<ui-combobox-item value="apple">Apple</ui-combobox-item>
|
|
103
|
+
<ui-combobox-item value="banana">Banana</ui-combobox-item>
|
|
104
|
+
</ui-combobox-list>
|
|
105
|
+
</ui-combobox-content>
|
|
106
|
+
</ui-combobox>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Autocomplete vs Combobox
|
|
110
|
+
|
|
111
|
+
Both filter a list as you type. They differ in what the value is.
|
|
112
|
+
|
|
113
|
+
- **Combobox**: the value is an option from the list, or several with `multiple`. The text is only a query; closing without choosing restores the selected label, and a form submits the value, not the text. Use it for a country, an assignee or a set of labels.
|
|
114
|
+
- **[Autocomplete](./autocomplete.md)**: the value is the text. Suggestions help fill it in, and anything typed is valid. Use it for a search box, an address or a free-form field.
|
|
115
|
+
|
|
116
|
+
`allow-custom-value` lets a combobox accept text that matches no option, like a datalist. Reach for it when choosing from the list is the main path; use an autocomplete when free text is.
|
|
117
|
+
|
|
118
|
+
## Build Your Own
|
|
119
|
+
|
|
120
|
+
`@ui-sdk/web/combobox` comes with ready-made `ui-combobox` elements. If you'd like to build your own, with your own element names, start from the `combobox` primitive in `@ui-sdk/core`:
|
|
121
|
+
|
|
122
|
+
```ts title="country-picker-elements.ts"
|
|
123
|
+
import { combobox } from "@ui-sdk/core/combobox"
|
|
124
|
+
import { createParts, wrappedChild } from "@ui-sdk/web"
|
|
125
|
+
|
|
126
|
+
createParts(combobox, {
|
|
127
|
+
root: "country-picker",
|
|
128
|
+
label: "country-label",
|
|
129
|
+
input: {
|
|
130
|
+
tag: "country-input",
|
|
131
|
+
target: wrappedChild("input"),
|
|
132
|
+
display: "contents",
|
|
133
|
+
},
|
|
134
|
+
content: "country-popup",
|
|
135
|
+
list: "country-list",
|
|
136
|
+
item: "country-option",
|
|
137
|
+
empty: "country-empty",
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<country-picker>
|
|
143
|
+
<country-label>Country</country-label>
|
|
144
|
+
<country-input><input /></country-input>
|
|
145
|
+
<country-popup hidden>
|
|
146
|
+
<country-empty>No countries found.</country-empty>
|
|
147
|
+
<country-list>
|
|
148
|
+
<country-option value="fr" label="France">France</country-option>
|
|
149
|
+
<country-option value="jp" label="Japan">Japan</country-option>
|
|
150
|
+
</country-list>
|
|
151
|
+
</country-popup>
|
|
152
|
+
</country-picker>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Each element keeps the behavior, state and data attributes of its ready-made counterpart: typing filters the options, arrow keys move the highlight and the popup opens in the top layer. See [Rendering](../rendering.md) and [Custom Parts](../custom-parts.md) for more.
|
|
156
|
+
|
|
157
|
+
## Disabled
|
|
158
|
+
|
|
159
|
+
A disabled combobox cannot be focused or changed and submits nothing. A read-only one remains focusable and submits its value, but does not open or accept changes. Keyboard navigation skips a disabled option, and it cannot be chosen.
|
|
160
|
+
|
|
161
|
+
```ts title="disabled.ts"
|
|
162
|
+
import "@ui-sdk/web/combobox"
|
|
163
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
164
|
+
|
|
165
|
+
const countries = [
|
|
166
|
+
{ value: "ar", label: "Argentina" },
|
|
167
|
+
{ value: "au", label: "Australia" },
|
|
168
|
+
{ value: "at", label: "Austria" },
|
|
169
|
+
{ value: "be", label: "Belgium" },
|
|
170
|
+
{ value: "br", label: "Brazil" },
|
|
171
|
+
{ value: "ca", label: "Canada" },
|
|
172
|
+
{ value: "cl", label: "Chile" },
|
|
173
|
+
{ value: "dk", label: "Denmark" },
|
|
174
|
+
{ value: "fi", label: "Finland" },
|
|
175
|
+
{ value: "fr", label: "France" },
|
|
176
|
+
{ value: "de", label: "Germany" },
|
|
177
|
+
{ value: "gr", label: "Greece" },
|
|
178
|
+
{ value: "jp", label: "Japan" },
|
|
179
|
+
{ value: "nl", label: "Netherlands" },
|
|
180
|
+
{ value: "es", label: "Spain" },
|
|
181
|
+
{ value: "se", label: "Sweden" },
|
|
182
|
+
{ value: "ch", label: "Switzerland" },
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
const countryLabel = (value: string) =>
|
|
186
|
+
countries.find((country) => country.value === value)?.label ?? value
|
|
187
|
+
|
|
188
|
+
const variants = [
|
|
189
|
+
{ label: "Billing country", attributes: 'default-value="fr" disabled' },
|
|
190
|
+
{ label: "Shipping from", attributes: 'default-value="fr" read-only' },
|
|
191
|
+
{ label: "Shipping to", attributes: "" },
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
function mount(root: HTMLElement) {
|
|
195
|
+
root.innerHTML = `
|
|
196
|
+
<div data-slot="stack">
|
|
197
|
+
${variants
|
|
198
|
+
.map(
|
|
199
|
+
(variant) => `
|
|
200
|
+
<ui-combobox ${variant.attributes}>
|
|
201
|
+
<div data-slot="stack">
|
|
202
|
+
<ui-combobox-label>${variant.label}</ui-combobox-label>
|
|
203
|
+
<div data-slot="row">
|
|
204
|
+
<ui-combobox-input>
|
|
205
|
+
<input placeholder="Search countries…" />
|
|
206
|
+
</ui-combobox-input>
|
|
207
|
+
<ui-combobox-trigger>
|
|
208
|
+
<button aria-label="Show countries">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
209
|
+
</ui-combobox-trigger>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
<ui-combobox-content hidden>
|
|
213
|
+
<ui-combobox-empty>No countries found.</ui-combobox-empty>
|
|
214
|
+
<ui-combobox-list>
|
|
215
|
+
${countries
|
|
216
|
+
.map(
|
|
217
|
+
(country) => `
|
|
218
|
+
<ui-combobox-item value="${country.value}" label="${country.label}" ${country.value === "dk" ? "disabled" : ""}>
|
|
219
|
+
${country.label}
|
|
220
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
221
|
+
</ui-combobox-item>
|
|
222
|
+
`
|
|
223
|
+
)
|
|
224
|
+
.join("")}
|
|
225
|
+
</ui-combobox-list>
|
|
226
|
+
</ui-combobox-content>
|
|
227
|
+
</ui-combobox>
|
|
228
|
+
`
|
|
229
|
+
)
|
|
230
|
+
.join("")}
|
|
231
|
+
</div>
|
|
232
|
+
`
|
|
233
|
+
for (const combobox of root.querySelectorAll<
|
|
234
|
+
HTMLElement & { itemToLabel: (value: string) => string }
|
|
235
|
+
>("ui-combobox")) {
|
|
236
|
+
combobox.itemToLabel = countryLabel
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export { mount }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Billing country is disabled and Shipping from is read-only. Open Shipping to: arrow keys skip Denmark, which can't be chosen.
|
|
244
|
+
|
|
245
|
+
## Clear and Trigger
|
|
246
|
+
|
|
247
|
+
`ui-combobox-clear` clears both the selection and the query, then keeps focus in the input. It appears while either has a value. When someone deletes all the text, a single selection clears right away instead of when the list closes. Escape brings the selection back while the list is still open; closing it any other way leaves it cleared.
|
|
248
|
+
|
|
249
|
+
```ts title="clear.ts"
|
|
250
|
+
import "@ui-sdk/web/combobox"
|
|
251
|
+
import { Check, ChevronDown, createElement, X } from "lucide"
|
|
252
|
+
import { log } from "./log"
|
|
253
|
+
|
|
254
|
+
const frameworks = [
|
|
255
|
+
{ value: "next", label: "Next.js" },
|
|
256
|
+
{ value: "sveltekit", label: "SvelteKit" },
|
|
257
|
+
{ value: "nuxt", label: "Nuxt" },
|
|
258
|
+
{ value: "remix", label: "Remix" },
|
|
259
|
+
{ value: "astro", label: "Astro" },
|
|
260
|
+
{ value: "solidstart", label: "SolidStart" },
|
|
261
|
+
{ value: "qwik", label: "Qwik City" },
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
const frameworkLabel = (value: string) =>
|
|
265
|
+
frameworks.find((framework) => framework.value === value)?.label ?? value
|
|
266
|
+
|
|
267
|
+
function mount(root: HTMLElement) {
|
|
268
|
+
root.innerHTML = `
|
|
269
|
+
<ui-combobox default-value="sveltekit">
|
|
270
|
+
<div data-slot="stack">
|
|
271
|
+
<ui-combobox-label>Framework</ui-combobox-label>
|
|
272
|
+
<div data-slot="row">
|
|
273
|
+
<ui-combobox-input>
|
|
274
|
+
<input placeholder="Select a framework" />
|
|
275
|
+
</ui-combobox-input>
|
|
276
|
+
<ui-combobox-clear>
|
|
277
|
+
<button aria-label="Clear framework">${createElement(X, { "aria-hidden": "true" }).outerHTML}</button>
|
|
278
|
+
</ui-combobox-clear>
|
|
279
|
+
<ui-combobox-trigger>
|
|
280
|
+
<button aria-label="Show frameworks">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
281
|
+
</ui-combobox-trigger>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
<ui-combobox-content hidden>
|
|
285
|
+
<ui-combobox-empty>No frameworks found.</ui-combobox-empty>
|
|
286
|
+
<ui-combobox-list>
|
|
287
|
+
${frameworks
|
|
288
|
+
.map(
|
|
289
|
+
(framework) => `
|
|
290
|
+
<ui-combobox-item value="${framework.value}" label="${framework.label}">
|
|
291
|
+
${framework.label}
|
|
292
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
293
|
+
</ui-combobox-item>
|
|
294
|
+
`
|
|
295
|
+
)
|
|
296
|
+
.join("")}
|
|
297
|
+
</ui-combobox-list>
|
|
298
|
+
</ui-combobox-content>
|
|
299
|
+
</ui-combobox>
|
|
300
|
+
`
|
|
301
|
+
const combobox = root.querySelector("ui-combobox")!
|
|
302
|
+
combobox.itemToLabel = (value) => frameworkLabel(value as string)
|
|
303
|
+
combobox.addEventListener("valuechange", (event) => {
|
|
304
|
+
const { value, reason } = event.detail
|
|
305
|
+
log(
|
|
306
|
+
root,
|
|
307
|
+
`${value ? frameworkLabel(value as string) : "Cleared"} (${reason})`
|
|
308
|
+
)
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export { mount }
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Choose a framework, then press the clear button. The selection and text clear, focus stays in the input, and the console logs each change.
|
|
316
|
+
|
|
317
|
+
## Auto Highlight
|
|
318
|
+
|
|
319
|
+
Add `auto-highlight` to highlight the first match while typing, so Enter can choose it immediately. With `auto-highlight="always"`, opening the list also highlights the selected item, or the first, so the arrow keys move from there. A highlight it placed follows the list as items arrive, and one the user moved stays while its item is there.
|
|
320
|
+
|
|
321
|
+
```ts title="auto-highlight.ts"
|
|
322
|
+
import "@ui-sdk/web/combobox"
|
|
323
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
324
|
+
|
|
325
|
+
const countries = [
|
|
326
|
+
{ value: "ar", label: "Argentina" },
|
|
327
|
+
{ value: "au", label: "Australia" },
|
|
328
|
+
{ value: "at", label: "Austria" },
|
|
329
|
+
{ value: "be", label: "Belgium" },
|
|
330
|
+
{ value: "br", label: "Brazil" },
|
|
331
|
+
{ value: "ca", label: "Canada" },
|
|
332
|
+
{ value: "cl", label: "Chile" },
|
|
333
|
+
{ value: "dk", label: "Denmark" },
|
|
334
|
+
{ value: "fi", label: "Finland" },
|
|
335
|
+
{ value: "fr", label: "France" },
|
|
336
|
+
{ value: "de", label: "Germany" },
|
|
337
|
+
{ value: "gr", label: "Greece" },
|
|
338
|
+
{ value: "jp", label: "Japan" },
|
|
339
|
+
{ value: "nl", label: "Netherlands" },
|
|
340
|
+
{ value: "es", label: "Spain" },
|
|
341
|
+
{ value: "se", label: "Sweden" },
|
|
342
|
+
{ value: "ch", label: "Switzerland" },
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
function mount(root: HTMLElement) {
|
|
346
|
+
root.innerHTML = `
|
|
347
|
+
<ui-combobox auto-highlight>
|
|
348
|
+
<div data-slot="stack">
|
|
349
|
+
<ui-combobox-label>Country</ui-combobox-label>
|
|
350
|
+
<div data-slot="row">
|
|
351
|
+
<ui-combobox-input>
|
|
352
|
+
<input placeholder="Type “sw” and press Enter" />
|
|
353
|
+
</ui-combobox-input>
|
|
354
|
+
<ui-combobox-trigger>
|
|
355
|
+
<button aria-label="Show countries">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
356
|
+
</ui-combobox-trigger>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
<ui-combobox-content hidden>
|
|
360
|
+
<ui-combobox-empty>No countries found.</ui-combobox-empty>
|
|
361
|
+
<ui-combobox-list>
|
|
362
|
+
${countries
|
|
363
|
+
.map(
|
|
364
|
+
(country) => `
|
|
365
|
+
<ui-combobox-item value="${country.value}" label="${country.label}">
|
|
366
|
+
${country.label}
|
|
367
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
368
|
+
</ui-combobox-item>
|
|
369
|
+
`
|
|
370
|
+
)
|
|
371
|
+
.join("")}
|
|
372
|
+
</ui-combobox-list>
|
|
373
|
+
</ui-combobox-content>
|
|
374
|
+
</ui-combobox>
|
|
375
|
+
`
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export { mount }
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Type “sw” and press Enter. The first matching country is already highlighted.
|
|
382
|
+
|
|
383
|
+
## Highlight Matches
|
|
384
|
+
|
|
385
|
+
`ui-combobox-item-text` marks the matching text. Matching ignores case and accents using the current locale, while the label keeps its original spelling. Each match is a `<mark>` you can style.
|
|
386
|
+
|
|
387
|
+
```ts title="highlight.ts"
|
|
388
|
+
import "@ui-sdk/web/combobox"
|
|
389
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
390
|
+
|
|
391
|
+
const countries = [
|
|
392
|
+
{ value: "ar", label: "Argentina", keywords: ["república argentina"] },
|
|
393
|
+
{ value: "au", label: "Australia", keywords: ["oz"] },
|
|
394
|
+
{ value: "at", label: "Austria", keywords: ["österreich"] },
|
|
395
|
+
{ value: "be", label: "Belgium", keywords: ["belgië", "belgique"] },
|
|
396
|
+
{ value: "br", label: "Brazil", keywords: ["brasil"] },
|
|
397
|
+
{ value: "ca", label: "Canada" },
|
|
398
|
+
{ value: "cl", label: "Chile" },
|
|
399
|
+
{ value: "dk", label: "Denmark", keywords: ["danmark"] },
|
|
400
|
+
{ value: "fi", label: "Finland", keywords: ["suomi"] },
|
|
401
|
+
{ value: "fr", label: "France" },
|
|
402
|
+
{ value: "de", label: "Germany", keywords: ["deutschland"] },
|
|
403
|
+
{ value: "gr", label: "Greece", keywords: ["hellas", "ελλάδα"] },
|
|
404
|
+
{ value: "jp", label: "Japan", keywords: ["nippon", "日本"] },
|
|
405
|
+
{ value: "nl", label: "Netherlands", keywords: ["holland", "nederland"] },
|
|
406
|
+
{ value: "es", label: "Spain", keywords: ["españa"] },
|
|
407
|
+
{ value: "se", label: "Sweden", keywords: ["sverige"] },
|
|
408
|
+
{
|
|
409
|
+
value: "ch",
|
|
410
|
+
label: "Switzerland",
|
|
411
|
+
keywords: ["schweiz", "suisse", "svizzera"],
|
|
412
|
+
},
|
|
413
|
+
]
|
|
414
|
+
|
|
415
|
+
function mount(root: HTMLElement) {
|
|
416
|
+
root.innerHTML = `
|
|
417
|
+
<ui-combobox>
|
|
418
|
+
<div data-slot="stack">
|
|
419
|
+
<ui-combobox-label>Country</ui-combobox-label>
|
|
420
|
+
<div data-slot="row">
|
|
421
|
+
<ui-combobox-input>
|
|
422
|
+
<input placeholder="Search countries…" />
|
|
423
|
+
</ui-combobox-input>
|
|
424
|
+
<ui-combobox-trigger>
|
|
425
|
+
<button aria-label="Show countries">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
426
|
+
</ui-combobox-trigger>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
<ui-combobox-content hidden>
|
|
430
|
+
<ui-combobox-empty>No countries found.</ui-combobox-empty>
|
|
431
|
+
<ui-combobox-list>
|
|
432
|
+
${countries
|
|
433
|
+
.map(
|
|
434
|
+
(country) => `
|
|
435
|
+
<ui-combobox-item value="${country.value}" label="${country.label}" ${country.keywords ? `keywords="${country.keywords.join(",")}"` : ""}>
|
|
436
|
+
<ui-combobox-item-text>${country.label}</ui-combobox-item-text>
|
|
437
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
438
|
+
</ui-combobox-item>
|
|
439
|
+
`
|
|
440
|
+
)
|
|
441
|
+
.join("")}
|
|
442
|
+
</ui-combobox-list>
|
|
443
|
+
</ui-combobox-content>
|
|
444
|
+
</ui-combobox>
|
|
445
|
+
`
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export { mount }
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Type “an”. Matching letters are marked while each country keeps its original spelling.
|
|
452
|
+
|
|
453
|
+
## Filter and Keywords
|
|
454
|
+
|
|
455
|
+
`filter` accepts `"contains"` (the default), `"startsWith"`, a function of the item and query, or `false` for results already filtered by a server. A function can return a score instead of `true` or `false`, and items you pass as `items` then sort by it, best first. `fuzzyScore` from `@ui-sdk/core/text-match` scores an exact match above a prefix, and a prefix above a match inside the label. Assign a function to the `filter` property.
|
|
456
|
+
|
|
457
|
+
Use `keywords` for text that should match without appearing in the label, such as a username or a country’s local name.
|
|
458
|
+
|
|
459
|
+
```ts title="filter.ts"
|
|
460
|
+
import "@ui-sdk/web/combobox"
|
|
461
|
+
import "@ui-sdk/web/select"
|
|
462
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
463
|
+
|
|
464
|
+
const countries = [
|
|
465
|
+
{ value: "ar", label: "Argentina", keywords: ["república argentina"] },
|
|
466
|
+
{ value: "au", label: "Australia" },
|
|
467
|
+
{ value: "at", label: "Austria", keywords: ["österreich"] },
|
|
468
|
+
{ value: "be", label: "Belgium", keywords: ["belgië", "belgique"] },
|
|
469
|
+
{ value: "br", label: "Brazil", keywords: ["brasil"] },
|
|
470
|
+
{ value: "ca", label: "Canada" },
|
|
471
|
+
{ value: "cl", label: "Chile" },
|
|
472
|
+
{ value: "dk", label: "Denmark", keywords: ["danmark"] },
|
|
473
|
+
{ value: "fi", label: "Finland", keywords: ["suomi"] },
|
|
474
|
+
{ value: "fr", label: "France" },
|
|
475
|
+
{ value: "de", label: "Germany", keywords: ["deutschland"] },
|
|
476
|
+
{ value: "gr", label: "Greece", keywords: ["hellas", "ελλάδα"] },
|
|
477
|
+
{ value: "jp", label: "Japan", keywords: ["nippon", "日本"] },
|
|
478
|
+
{ value: "nl", label: "Netherlands", keywords: ["holland", "nederland"] },
|
|
479
|
+
{ value: "es", label: "Spain", keywords: ["españa"] },
|
|
480
|
+
{ value: "se", label: "Sweden", keywords: ["sverige"] },
|
|
481
|
+
{
|
|
482
|
+
value: "ch",
|
|
483
|
+
label: "Switzerland",
|
|
484
|
+
keywords: ["schweiz", "suisse", "svizzera"],
|
|
485
|
+
},
|
|
486
|
+
]
|
|
487
|
+
|
|
488
|
+
function fuzzy(item: { label: string }, query: string) {
|
|
489
|
+
let at = 0
|
|
490
|
+
for (const char of item.label.toLowerCase()) {
|
|
491
|
+
if (char === query[at]?.toLowerCase()) {
|
|
492
|
+
at++
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return at === query.length
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const modes = [
|
|
499
|
+
{ value: "contains", label: "Contains" },
|
|
500
|
+
{ value: "startsWith", label: "Starts with" },
|
|
501
|
+
{ value: "fuzzy", label: "Fuzzy" },
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
function mount(root: HTMLElement) {
|
|
505
|
+
root.innerHTML = `
|
|
506
|
+
<div data-slot="stack">
|
|
507
|
+
<div data-slot="controls">
|
|
508
|
+
<ui-select default-value="contains">
|
|
509
|
+
<ui-select-label>Filter</ui-select-label>
|
|
510
|
+
<ui-select-trigger>
|
|
511
|
+
<button>
|
|
512
|
+
<ui-select-value></ui-select-value>
|
|
513
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
514
|
+
</button>
|
|
515
|
+
</ui-select-trigger>
|
|
516
|
+
<ui-select-content hidden>
|
|
517
|
+
${modes
|
|
518
|
+
.map(
|
|
519
|
+
(option) => `
|
|
520
|
+
<ui-select-item value="${option.value}">
|
|
521
|
+
${option.label}
|
|
522
|
+
<ui-select-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-select-item-indicator>
|
|
523
|
+
</ui-select-item>
|
|
524
|
+
`
|
|
525
|
+
)
|
|
526
|
+
.join("")}
|
|
527
|
+
</ui-select-content>
|
|
528
|
+
</ui-select>
|
|
529
|
+
</div>
|
|
530
|
+
<ui-combobox filter="contains">
|
|
531
|
+
<div data-slot="stack">
|
|
532
|
+
<ui-combobox-label>Country</ui-combobox-label>
|
|
533
|
+
<div data-slot="row">
|
|
534
|
+
<ui-combobox-input>
|
|
535
|
+
<input placeholder="Try “an”, “swd” or “deutschland”" />
|
|
536
|
+
</ui-combobox-input>
|
|
537
|
+
<ui-combobox-trigger>
|
|
538
|
+
<button aria-label="Show countries">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
539
|
+
</ui-combobox-trigger>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
<ui-combobox-content hidden>
|
|
543
|
+
<ui-combobox-empty>No countries found.</ui-combobox-empty>
|
|
544
|
+
<ui-combobox-list>
|
|
545
|
+
${countries
|
|
546
|
+
.map(
|
|
547
|
+
(country) => `
|
|
548
|
+
<ui-combobox-item value="${country.value}" label="${country.label}"${country.keywords ? ` keywords="${country.keywords.join(",")}"` : ""}>
|
|
549
|
+
${country.label}
|
|
550
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
551
|
+
</ui-combobox-item>
|
|
552
|
+
`
|
|
553
|
+
)
|
|
554
|
+
.join("")}
|
|
555
|
+
</ui-combobox-list>
|
|
556
|
+
</ui-combobox-content>
|
|
557
|
+
</ui-combobox>
|
|
558
|
+
</div>
|
|
559
|
+
`
|
|
560
|
+
const combobox = root.querySelector("ui-combobox")!
|
|
561
|
+
root.querySelector("ui-select")!.addEventListener("valuechange", (event) => {
|
|
562
|
+
const mode = event.detail.value
|
|
563
|
+
if (!mode) {
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
if (mode === "fuzzy") {
|
|
567
|
+
combobox.filter = fuzzy
|
|
568
|
+
} else {
|
|
569
|
+
combobox.filter = undefined
|
|
570
|
+
combobox.setAttribute("filter", String(mode))
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export { mount }
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Type “an” and switch Filter between Contains and Starts with. Choose Fuzzy and type “swd” for Sweden, or type “deutschland” to find Germany through a keyword.
|
|
579
|
+
|
|
580
|
+
## Groups
|
|
581
|
+
|
|
582
|
+
Use `ui-combobox-group` and `ui-combobox-group-label` to name groups. Filtering hides empty groups and separators as their items disappear.
|
|
583
|
+
|
|
584
|
+
```ts title="groups.ts"
|
|
585
|
+
import "@ui-sdk/web/combobox"
|
|
586
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
587
|
+
|
|
588
|
+
const timezones = [
|
|
589
|
+
{
|
|
590
|
+
label: "Americas",
|
|
591
|
+
items: [
|
|
592
|
+
{ value: "America/New_York", label: "New York" },
|
|
593
|
+
{ value: "America/Chicago", label: "Chicago" },
|
|
594
|
+
{ value: "America/Denver", label: "Denver" },
|
|
595
|
+
{ value: "America/Los_Angeles", label: "Los Angeles" },
|
|
596
|
+
{ value: "America/Sao_Paulo", label: "São Paulo" },
|
|
597
|
+
],
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
label: "Europe",
|
|
601
|
+
items: [
|
|
602
|
+
{ value: "Europe/London", label: "London" },
|
|
603
|
+
{ value: "Europe/Paris", label: "Paris" },
|
|
604
|
+
{ value: "Europe/Berlin", label: "Berlin" },
|
|
605
|
+
{ value: "Europe/Madrid", label: "Madrid" },
|
|
606
|
+
],
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
label: "Asia & Pacific",
|
|
610
|
+
items: [
|
|
611
|
+
{ value: "Asia/Tokyo", label: "Tokyo" },
|
|
612
|
+
{ value: "Asia/Singapore", label: "Singapore" },
|
|
613
|
+
{ value: "Asia/Kolkata", label: "Kolkata" },
|
|
614
|
+
{ value: "Australia/Sydney", label: "Sydney" },
|
|
615
|
+
],
|
|
616
|
+
},
|
|
617
|
+
]
|
|
618
|
+
|
|
619
|
+
function mount(root: HTMLElement) {
|
|
620
|
+
root.innerHTML = `
|
|
621
|
+
<ui-combobox>
|
|
622
|
+
<div data-slot="stack">
|
|
623
|
+
<ui-combobox-label>Timezone</ui-combobox-label>
|
|
624
|
+
<div data-slot="row">
|
|
625
|
+
<ui-combobox-input>
|
|
626
|
+
<input placeholder="Search cities…" />
|
|
627
|
+
</ui-combobox-input>
|
|
628
|
+
<ui-combobox-trigger>
|
|
629
|
+
<button aria-label="Show timezones">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
630
|
+
</ui-combobox-trigger>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
<ui-combobox-content hidden>
|
|
634
|
+
<ui-combobox-empty>No cities found.</ui-combobox-empty>
|
|
635
|
+
<ui-combobox-list>
|
|
636
|
+
${timezones
|
|
637
|
+
.map(
|
|
638
|
+
(group, index) => `
|
|
639
|
+
${index > 0 ? `<ui-combobox-separator></ui-combobox-separator>` : ""}
|
|
640
|
+
<ui-combobox-group>
|
|
641
|
+
<ui-combobox-group-label>${group.label}</ui-combobox-group-label>
|
|
642
|
+
${group.items
|
|
643
|
+
.map(
|
|
644
|
+
(item) => `
|
|
645
|
+
<ui-combobox-item value="${item.value}" label="${item.label}">
|
|
646
|
+
${item.label}
|
|
647
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
648
|
+
</ui-combobox-item>
|
|
649
|
+
`
|
|
650
|
+
)
|
|
651
|
+
.join("")}
|
|
652
|
+
</ui-combobox-group>
|
|
653
|
+
`
|
|
654
|
+
)
|
|
655
|
+
.join("")}
|
|
656
|
+
</ui-combobox-list>
|
|
657
|
+
</ui-combobox-content>
|
|
658
|
+
</ui-combobox>
|
|
659
|
+
`
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export { mount }
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
Type “chic”. Chicago remains in Americas; groups with no matches disappear.
|
|
666
|
+
|
|
667
|
+
## Icons
|
|
668
|
+
|
|
669
|
+
`ui-combobox-item-icon` keeps decorative icons out of accessible names, filtering, and match marks. `ui-combobox-value-icon` projects the selected item’s icon next to the input; it takes no children and is only for a single selection.
|
|
670
|
+
|
|
671
|
+
Wrap the input and icon together, then use that wrapper as the popup’s `anchor`. Add `mirror` for directional icons. An icon-only option still needs an `aria-label`.
|
|
672
|
+
|
|
673
|
+
```ts title="icons.ts"
|
|
674
|
+
import "@ui-sdk/web/combobox"
|
|
675
|
+
import {
|
|
676
|
+
ArrowRight,
|
|
677
|
+
Check,
|
|
678
|
+
ChevronDown,
|
|
679
|
+
Circle,
|
|
680
|
+
CircleCheck,
|
|
681
|
+
CircleX,
|
|
682
|
+
createElement,
|
|
683
|
+
} from "lucide"
|
|
684
|
+
|
|
685
|
+
function mount(root: HTMLElement) {
|
|
686
|
+
root.innerHTML = `
|
|
687
|
+
<ui-combobox>
|
|
688
|
+
<div data-slot="stack">
|
|
689
|
+
<ui-combobox-label>Status</ui-combobox-label>
|
|
690
|
+
<div id="combobox-icons-field" data-slot="row">
|
|
691
|
+
<ui-combobox-value-icon></ui-combobox-value-icon>
|
|
692
|
+
<ui-combobox-input>
|
|
693
|
+
<input placeholder="Select a status" />
|
|
694
|
+
</ui-combobox-input>
|
|
695
|
+
<ui-combobox-trigger>
|
|
696
|
+
<button aria-label="Show statuses">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
697
|
+
</ui-combobox-trigger>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
<ui-combobox-content hidden anchor="#combobox-icons-field">
|
|
701
|
+
<ui-combobox-empty>No statuses found.</ui-combobox-empty>
|
|
702
|
+
<ui-combobox-list>
|
|
703
|
+
<ui-combobox-item value="backlog" label="Backlog">
|
|
704
|
+
<ui-combobox-item-icon>${createElement(Circle, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-icon>
|
|
705
|
+
<ui-combobox-item-text>Backlog</ui-combobox-item-text>
|
|
706
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
707
|
+
</ui-combobox-item>
|
|
708
|
+
<ui-combobox-item value="in-progress" label="In progress">
|
|
709
|
+
<ui-combobox-item-icon mirror>${createElement(ArrowRight, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-icon>
|
|
710
|
+
<ui-combobox-item-text>In progress</ui-combobox-item-text>
|
|
711
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
712
|
+
</ui-combobox-item>
|
|
713
|
+
<ui-combobox-item value="done" label="Done">
|
|
714
|
+
<ui-combobox-item-icon>${createElement(CircleCheck, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-icon>
|
|
715
|
+
<ui-combobox-item-text>Done</ui-combobox-item-text>
|
|
716
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
717
|
+
</ui-combobox-item>
|
|
718
|
+
<ui-combobox-item value="canceled" label="Canceled">
|
|
719
|
+
<ui-combobox-item-icon>${createElement(CircleX, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-icon>
|
|
720
|
+
<ui-combobox-item-text>Canceled</ui-combobox-item-text>
|
|
721
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
722
|
+
</ui-combobox-item>
|
|
723
|
+
</ui-combobox-list>
|
|
724
|
+
</ui-combobox-content>
|
|
725
|
+
</ui-combobox>
|
|
726
|
+
`
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export { mount }
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Choose Done. Its icon appears next to the input as well as in the list.
|
|
733
|
+
|
|
734
|
+
## Custom Items
|
|
735
|
+
|
|
736
|
+
Show useful context beside a label, such as a username or role. Set `label` explicitly so filtering and the input use the intended text, and put extra search terms in `keywords`.
|
|
737
|
+
|
|
738
|
+
```ts title="custom-items.ts"
|
|
739
|
+
import "@ui-sdk/web/combobox"
|
|
740
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
741
|
+
|
|
742
|
+
const users = [
|
|
743
|
+
{ id: 1, name: "Hollis Grant", username: "hollis", role: "Owner" },
|
|
744
|
+
{ id: 2, name: "Inès Moreau", username: "ines", role: "Admin" },
|
|
745
|
+
{ id: 3, name: "Kofi Mensah", username: "kofi", role: "Member" },
|
|
746
|
+
{ id: 4, name: "Wren Takahashi", username: "wren", role: "Member" },
|
|
747
|
+
{ id: 5, name: "Tomasz Wójcik", username: "tomasz", role: "Guest" },
|
|
748
|
+
]
|
|
749
|
+
|
|
750
|
+
function mount(root: HTMLElement) {
|
|
751
|
+
root.innerHTML = `
|
|
752
|
+
<ui-combobox>
|
|
753
|
+
<div data-slot="stack">
|
|
754
|
+
<ui-combobox-label>Owner</ui-combobox-label>
|
|
755
|
+
<div data-slot="row">
|
|
756
|
+
<ui-combobox-input>
|
|
757
|
+
<input placeholder="Search people…" />
|
|
758
|
+
</ui-combobox-input>
|
|
759
|
+
<ui-combobox-trigger>
|
|
760
|
+
<button aria-label="Show people">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
761
|
+
</ui-combobox-trigger>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
<ui-combobox-content hidden>
|
|
765
|
+
<ui-combobox-empty>No people found.</ui-combobox-empty>
|
|
766
|
+
<ui-combobox-list>
|
|
767
|
+
${users
|
|
768
|
+
.map(
|
|
769
|
+
(user) => `
|
|
770
|
+
<ui-combobox-item value="${user.username}" label="${user.name}" keywords="${user.username}">
|
|
771
|
+
<span data-slot="badge">${user.name.slice(0, 1)}</span>
|
|
772
|
+
<span data-slot="stack">
|
|
773
|
+
<span>${user.name}</span>
|
|
774
|
+
<span data-slot="hint">@${user.username}</span>
|
|
775
|
+
</span>
|
|
776
|
+
<span data-slot="hint">${user.role}</span>
|
|
777
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
778
|
+
</ui-combobox-item>
|
|
779
|
+
`
|
|
780
|
+
)
|
|
781
|
+
.join("")}
|
|
782
|
+
</ui-combobox-list>
|
|
783
|
+
</ui-combobox-content>
|
|
784
|
+
</ui-combobox>
|
|
785
|
+
`
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export { mount }
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
Search for a person’s name or username. Choose a result to put that person’s name in the input.
|
|
792
|
+
|
|
793
|
+
## Items as Data
|
|
794
|
+
|
|
795
|
+
Pass `items` when the combobox needs the whole collection, such as recent items, server results, or virtualization. Assign it as a property; the element renders bare parts for you, and a `<template>` styles them. Each item's `label` supplies the text for filtering and the input; `keywords` and `disabled` work the same as with items you render yourself. [Lists and Menus](../lists-and-menus.md) covers the `ListData` format.
|
|
796
|
+
|
|
797
|
+
```ts
|
|
798
|
+
const combobox = document.querySelector("ui-combobox")!
|
|
799
|
+
combobox.items = [
|
|
800
|
+
{ value: "apple", label: "Apple" },
|
|
801
|
+
{ value: "cherry", label: "Cherry", keywords: ["red"] },
|
|
802
|
+
{
|
|
803
|
+
type: "group",
|
|
804
|
+
label: "Citrus",
|
|
805
|
+
items: [{ value: "lemon", label: "Lemon" }],
|
|
806
|
+
},
|
|
807
|
+
]
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
Items can change while the list is open. Selection and highlight stay on their items; removing the highlighted item clears the highlight.
|
|
811
|
+
|
|
812
|
+
## Recent Items
|
|
813
|
+
|
|
814
|
+
`emptyItems` supplies suggestions while the query is empty. Pair it with `items` for the full searchable collection. Assign both as properties; the template renders whichever collection is active.
|
|
815
|
+
|
|
816
|
+
```ts title="recent.ts"
|
|
817
|
+
import "@ui-sdk/web/combobox"
|
|
818
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
819
|
+
|
|
820
|
+
const countries = [
|
|
821
|
+
{ value: "ar", label: "Argentina", keywords: ["república argentina"] },
|
|
822
|
+
{ value: "au", label: "Australia", keywords: ["oz"] },
|
|
823
|
+
{ value: "at", label: "Austria", keywords: ["österreich"] },
|
|
824
|
+
{ value: "be", label: "Belgium", keywords: ["belgië", "belgique"] },
|
|
825
|
+
{ value: "br", label: "Brazil", keywords: ["brasil"] },
|
|
826
|
+
{ value: "ca", label: "Canada" },
|
|
827
|
+
{ value: "cl", label: "Chile" },
|
|
828
|
+
{ value: "dk", label: "Denmark", keywords: ["danmark"] },
|
|
829
|
+
{ value: "fi", label: "Finland", keywords: ["suomi"] },
|
|
830
|
+
{ value: "fr", label: "France" },
|
|
831
|
+
{ value: "de", label: "Germany", keywords: ["deutschland"] },
|
|
832
|
+
{ value: "gr", label: "Greece", keywords: ["hellas", "ελλάδα"] },
|
|
833
|
+
{ value: "jp", label: "Japan", keywords: ["nippon", "日本"] },
|
|
834
|
+
{ value: "nl", label: "Netherlands", keywords: ["holland", "nederland"] },
|
|
835
|
+
{ value: "es", label: "Spain", keywords: ["españa"] },
|
|
836
|
+
{ value: "se", label: "Sweden", keywords: ["sverige"] },
|
|
837
|
+
{
|
|
838
|
+
value: "ch",
|
|
839
|
+
label: "Switzerland",
|
|
840
|
+
keywords: ["schweiz", "suisse", "svizzera"],
|
|
841
|
+
},
|
|
842
|
+
]
|
|
843
|
+
|
|
844
|
+
const recentCountries = [
|
|
845
|
+
{
|
|
846
|
+
type: "group" as const,
|
|
847
|
+
label: "Recent",
|
|
848
|
+
items: [countries[9], countries[12], countries[15]],
|
|
849
|
+
},
|
|
850
|
+
]
|
|
851
|
+
|
|
852
|
+
function mount(root: HTMLElement) {
|
|
853
|
+
root.innerHTML = `
|
|
854
|
+
<ui-combobox>
|
|
855
|
+
<div data-slot="stack">
|
|
856
|
+
<ui-combobox-label>Country</ui-combobox-label>
|
|
857
|
+
<div data-slot="row">
|
|
858
|
+
<ui-combobox-input>
|
|
859
|
+
<input placeholder="Search countries…" />
|
|
860
|
+
</ui-combobox-input>
|
|
861
|
+
<ui-combobox-trigger>
|
|
862
|
+
<button aria-label="Show countries">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
863
|
+
</ui-combobox-trigger>
|
|
864
|
+
</div>
|
|
865
|
+
</div>
|
|
866
|
+
<ui-combobox-content hidden>
|
|
867
|
+
<ui-combobox-empty>No countries found.</ui-combobox-empty>
|
|
868
|
+
<ui-combobox-list></ui-combobox-list>
|
|
869
|
+
</ui-combobox-content>
|
|
870
|
+
<template>
|
|
871
|
+
<ui-combobox-group>
|
|
872
|
+
<ui-combobox-group-label></ui-combobox-group-label>
|
|
873
|
+
<ui-combobox-item>
|
|
874
|
+
<ui-combobox-item-text></ui-combobox-item-text>
|
|
875
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
876
|
+
</ui-combobox-item>
|
|
877
|
+
</ui-combobox-group>
|
|
878
|
+
</template>
|
|
879
|
+
</ui-combobox>
|
|
880
|
+
`
|
|
881
|
+
const combobox = root.querySelector("ui-combobox")!
|
|
882
|
+
combobox.emptyItems = recentCountries
|
|
883
|
+
combobox.announcement = (change) =>
|
|
884
|
+
change.type === "results" ? `${change.count} countries match` : null
|
|
885
|
+
combobox.items = countries
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export { mount }
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
Open the empty input to see Recent. Start typing to search all countries; clear the text to return to Recent.
|
|
892
|
+
|
|
893
|
+
## Object Values
|
|
894
|
+
|
|
895
|
+
Use the `itemToValue` property for an object’s string identity and form value, `itemToLabel` for its displayed text, and `isItemEqualToValue` when two different objects can represent the same option.
|
|
896
|
+
|
|
897
|
+
```html
|
|
898
|
+
<ui-combobox name="reviewer">…</ui-combobox>
|
|
899
|
+
|
|
900
|
+
<script type="module">
|
|
901
|
+
const reviewer = document.querySelector("ui-combobox[name=reviewer]")
|
|
902
|
+
reviewer.itemToValue = (person) => String(person.id)
|
|
903
|
+
reviewer.itemToLabel = (person) => person.name
|
|
904
|
+
reviewer.isItemEqualToValue = (person, value) => person.id === value.id
|
|
905
|
+
</script>
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
## Multiple
|
|
909
|
+
|
|
910
|
+
Add `multiple` to make the value an array. Choosing an option toggles it, keeps the list open, and clears the query. Put the input inside `ui-combobox-chips`; the element renders each value as a chip with a removal button, copying the `ui-combobox-chip` in a `<template>` when you write one.
|
|
911
|
+
|
|
912
|
+
Backspace in an empty input removes the last chip in one press. Unlike [Token Input](./token-input.md#deleting-tokens), there is no `token-deletion` option to select it first. Arrow keys move between chips. Set the `itemToLabel` property when starting with values whose labels are not yet known from rendered items.
|
|
913
|
+
|
|
914
|
+
Add `keep-query` to choose several matches without retyping the query.
|
|
915
|
+
|
|
916
|
+
```ts title="multiple.ts"
|
|
917
|
+
import "@ui-sdk/web/combobox"
|
|
918
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
919
|
+
|
|
920
|
+
const frameworks = [
|
|
921
|
+
{ value: "next", label: "Next.js" },
|
|
922
|
+
{ value: "sveltekit", label: "SvelteKit" },
|
|
923
|
+
{ value: "nuxt", label: "Nuxt" },
|
|
924
|
+
{ value: "remix", label: "Remix" },
|
|
925
|
+
{ value: "astro", label: "Astro" },
|
|
926
|
+
{ value: "solidstart", label: "SolidStart" },
|
|
927
|
+
{ value: "qwik", label: "Qwik City" },
|
|
928
|
+
]
|
|
929
|
+
|
|
930
|
+
const frameworkLabel = (value: string) =>
|
|
931
|
+
frameworks.find((framework) => framework.value === value)?.label ?? value
|
|
932
|
+
|
|
933
|
+
function mount(root: HTMLElement) {
|
|
934
|
+
root.innerHTML = `
|
|
935
|
+
<ui-combobox multiple default-value="next, astro">
|
|
936
|
+
<div data-slot="stack">
|
|
937
|
+
<ui-combobox-label>Frameworks</ui-combobox-label>
|
|
938
|
+
<div data-slot="row">
|
|
939
|
+
<ui-combobox-chips>
|
|
940
|
+
<ui-combobox-input>
|
|
941
|
+
<input placeholder="Add a framework…" />
|
|
942
|
+
</ui-combobox-input>
|
|
943
|
+
</ui-combobox-chips>
|
|
944
|
+
<ui-combobox-trigger>
|
|
945
|
+
<button aria-label="Show frameworks">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
946
|
+
</ui-combobox-trigger>
|
|
947
|
+
</div>
|
|
948
|
+
</div>
|
|
949
|
+
<ui-combobox-content hidden>
|
|
950
|
+
<ui-combobox-empty>No frameworks found.</ui-combobox-empty>
|
|
951
|
+
<ui-combobox-list>
|
|
952
|
+
${frameworks
|
|
953
|
+
.map(
|
|
954
|
+
(framework) => `
|
|
955
|
+
<ui-combobox-item value="${framework.value}" label="${framework.label}">
|
|
956
|
+
${framework.label}
|
|
957
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
958
|
+
</ui-combobox-item>
|
|
959
|
+
`
|
|
960
|
+
)
|
|
961
|
+
.join("")}
|
|
962
|
+
</ui-combobox-list>
|
|
963
|
+
</ui-combobox-content>
|
|
964
|
+
<template>
|
|
965
|
+
<ui-combobox-chip>
|
|
966
|
+
<ui-combobox-chip-remove><button type="button">×</button></ui-combobox-chip-remove>
|
|
967
|
+
</ui-combobox-chip>
|
|
968
|
+
</template>
|
|
969
|
+
</ui-combobox>
|
|
970
|
+
`
|
|
971
|
+
const combobox = root.querySelector("ui-combobox") as HTMLElement & {
|
|
972
|
+
itemToLabel: (value: string) => string
|
|
973
|
+
}
|
|
974
|
+
combobox.itemToLabel = frameworkLabel
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export { mount }
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
Choose several frameworks. Remove one with its chip button, or press Backspace in the empty input to remove the last chip.
|
|
981
|
+
|
|
982
|
+
## Select All
|
|
983
|
+
|
|
984
|
+
`ui-combobox-select-all` toggles every matching, enabled option. Inside a group, it affects only that group. It joins keyboard navigation and reports a mixed state while some of its options are selected.
|
|
985
|
+
|
|
986
|
+
```ts title="select-all.ts"
|
|
987
|
+
import "@ui-sdk/web/combobox"
|
|
988
|
+
import { Check, createElement, Minus } from "lucide"
|
|
989
|
+
|
|
990
|
+
const timezones = [
|
|
991
|
+
{
|
|
992
|
+
label: "Americas",
|
|
993
|
+
items: [
|
|
994
|
+
{ value: "America/New_York", label: "New York" },
|
|
995
|
+
{ value: "America/Chicago", label: "Chicago" },
|
|
996
|
+
{ value: "America/Denver", label: "Denver" },
|
|
997
|
+
{ value: "America/Los_Angeles", label: "Los Angeles" },
|
|
998
|
+
{ value: "America/Sao_Paulo", label: "São Paulo" },
|
|
999
|
+
],
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
label: "Europe",
|
|
1003
|
+
items: [
|
|
1004
|
+
{ value: "Europe/London", label: "London" },
|
|
1005
|
+
{ value: "Europe/Paris", label: "Paris" },
|
|
1006
|
+
{ value: "Europe/Berlin", label: "Berlin" },
|
|
1007
|
+
{ value: "Europe/Madrid", label: "Madrid" },
|
|
1008
|
+
],
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
label: "Asia & Pacific",
|
|
1012
|
+
items: [
|
|
1013
|
+
{ value: "Asia/Tokyo", label: "Tokyo" },
|
|
1014
|
+
{ value: "Asia/Singapore", label: "Singapore" },
|
|
1015
|
+
{ value: "Asia/Kolkata", label: "Kolkata" },
|
|
1016
|
+
{ value: "Australia/Sydney", label: "Sydney" },
|
|
1017
|
+
],
|
|
1018
|
+
},
|
|
1019
|
+
]
|
|
1020
|
+
|
|
1021
|
+
function mount(root: HTMLElement) {
|
|
1022
|
+
root.innerHTML = `
|
|
1023
|
+
<ui-combobox multiple default-value="Europe/Paris">
|
|
1024
|
+
<div data-slot="stack">
|
|
1025
|
+
<ui-combobox-label>Timezones</ui-combobox-label>
|
|
1026
|
+
<ui-combobox-chips>
|
|
1027
|
+
<ui-combobox-input>
|
|
1028
|
+
<input placeholder="Add timezones…" />
|
|
1029
|
+
</ui-combobox-input>
|
|
1030
|
+
</ui-combobox-chips>
|
|
1031
|
+
</div>
|
|
1032
|
+
<ui-combobox-content hidden>
|
|
1033
|
+
<ui-combobox-empty>No timezones found.</ui-combobox-empty>
|
|
1034
|
+
<ui-combobox-list>
|
|
1035
|
+
<ui-combobox-select-all>
|
|
1036
|
+
<span>
|
|
1037
|
+
${createElement(Check, { "aria-hidden": "true" }).outerHTML}
|
|
1038
|
+
${createElement(Minus, { "aria-hidden": "true" }).outerHTML}
|
|
1039
|
+
</span>
|
|
1040
|
+
Select all
|
|
1041
|
+
</ui-combobox-select-all>
|
|
1042
|
+
${timezones
|
|
1043
|
+
.map(
|
|
1044
|
+
(group) => `
|
|
1045
|
+
<ui-combobox-group>
|
|
1046
|
+
<ui-combobox-group-label>${group.label}</ui-combobox-group-label>
|
|
1047
|
+
<ui-combobox-select-all>
|
|
1048
|
+
<span data-hide="checked">Select all in ${group.label}</span>
|
|
1049
|
+
<span data-show="checked">Clear ${group.label}</span>
|
|
1050
|
+
</ui-combobox-select-all>
|
|
1051
|
+
${group.items
|
|
1052
|
+
.map(
|
|
1053
|
+
(zone) => `
|
|
1054
|
+
<ui-combobox-item value="${zone.value}" label="${zone.label}">
|
|
1055
|
+
<span>
|
|
1056
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
1057
|
+
</span>
|
|
1058
|
+
${zone.label}
|
|
1059
|
+
</ui-combobox-item>
|
|
1060
|
+
`
|
|
1061
|
+
)
|
|
1062
|
+
.join("")}
|
|
1063
|
+
</ui-combobox-group>
|
|
1064
|
+
`
|
|
1065
|
+
)
|
|
1066
|
+
.join("")}
|
|
1067
|
+
</ui-combobox-list>
|
|
1068
|
+
</ui-combobox-content>
|
|
1069
|
+
<template>
|
|
1070
|
+
<ui-combobox-chip>
|
|
1071
|
+
<ui-combobox-chip-remove><button type="button">×</button></ui-combobox-chip-remove>
|
|
1072
|
+
</ui-combobox-chip>
|
|
1073
|
+
</template>
|
|
1074
|
+
</ui-combobox>
|
|
1075
|
+
`
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
export { mount }
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
Open the list and choose a group’s select-all option. Type to narrow the matches, then use the main select-all option on those results.
|
|
1082
|
+
|
|
1083
|
+
## Chips That Collapse
|
|
1084
|
+
|
|
1085
|
+
Add `ui-combobox-chips-overflow` to keep chips on one line. It measures which chips fit, hides the rest, and exposes their count and labels. Separately, the `valuechange` event gives you the selection for a short text summary.
|
|
1086
|
+
|
|
1087
|
+
```ts title="chips-overflow.ts"
|
|
1088
|
+
import "@ui-sdk/web/combobox"
|
|
1089
|
+
import { Check, createElement } from "lucide"
|
|
1090
|
+
|
|
1091
|
+
const countries = [
|
|
1092
|
+
{ value: "ar", label: "Argentina", keywords: ["república argentina"] },
|
|
1093
|
+
{ value: "au", label: "Australia", keywords: ["oz"] },
|
|
1094
|
+
{ value: "at", label: "Austria", keywords: ["österreich"] },
|
|
1095
|
+
{ value: "be", label: "Belgium", keywords: ["belgië", "belgique"] },
|
|
1096
|
+
{ value: "br", label: "Brazil", keywords: ["brasil"] },
|
|
1097
|
+
{ value: "ca", label: "Canada" },
|
|
1098
|
+
{ value: "cl", label: "Chile" },
|
|
1099
|
+
{ value: "dk", label: "Denmark", keywords: ["danmark"] },
|
|
1100
|
+
{ value: "fi", label: "Finland", keywords: ["suomi"] },
|
|
1101
|
+
{ value: "fr", label: "France" },
|
|
1102
|
+
{ value: "de", label: "Germany", keywords: ["deutschland"] },
|
|
1103
|
+
{ value: "gr", label: "Greece", keywords: ["hellas", "ελλάδα"] },
|
|
1104
|
+
{ value: "jp", label: "Japan", keywords: ["nippon", "日本"] },
|
|
1105
|
+
{ value: "nl", label: "Netherlands", keywords: ["holland", "nederland"] },
|
|
1106
|
+
{ value: "es", label: "Spain", keywords: ["españa"] },
|
|
1107
|
+
{ value: "se", label: "Sweden", keywords: ["sverige"] },
|
|
1108
|
+
{
|
|
1109
|
+
value: "ch",
|
|
1110
|
+
label: "Switzerland",
|
|
1111
|
+
keywords: ["schweiz", "suisse", "svizzera"],
|
|
1112
|
+
},
|
|
1113
|
+
]
|
|
1114
|
+
|
|
1115
|
+
const countryLabel = (value: string) =>
|
|
1116
|
+
countries.find((country) => country.value === value)?.label ?? value
|
|
1117
|
+
|
|
1118
|
+
function mount(root: HTMLElement) {
|
|
1119
|
+
root.innerHTML = `
|
|
1120
|
+
<ui-combobox multiple default-value="fr, jp, se, de, ch">
|
|
1121
|
+
<div data-slot="stack">
|
|
1122
|
+
<ui-combobox-label>Countries</ui-combobox-label>
|
|
1123
|
+
<ui-combobox-chips>
|
|
1124
|
+
<ui-combobox-chips-overflow></ui-combobox-chips-overflow>
|
|
1125
|
+
<ui-combobox-input>
|
|
1126
|
+
<input placeholder="Add…" />
|
|
1127
|
+
</ui-combobox-input>
|
|
1128
|
+
</ui-combobox-chips>
|
|
1129
|
+
<p data-slot="hint"></p>
|
|
1130
|
+
</div>
|
|
1131
|
+
<ui-combobox-content hidden>
|
|
1132
|
+
<ui-combobox-empty>No countries found.</ui-combobox-empty>
|
|
1133
|
+
<ui-combobox-list>
|
|
1134
|
+
${countries
|
|
1135
|
+
.map(
|
|
1136
|
+
(country) => `
|
|
1137
|
+
<ui-combobox-item value="${country.value}" label="${country.label}">
|
|
1138
|
+
${country.label}
|
|
1139
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
1140
|
+
</ui-combobox-item>
|
|
1141
|
+
`
|
|
1142
|
+
)
|
|
1143
|
+
.join("")}
|
|
1144
|
+
</ui-combobox-list>
|
|
1145
|
+
</ui-combobox-content>
|
|
1146
|
+
<template>
|
|
1147
|
+
<ui-combobox-chip>
|
|
1148
|
+
<ui-combobox-chip-remove><button type="button">×</button></ui-combobox-chip-remove>
|
|
1149
|
+
</ui-combobox-chip>
|
|
1150
|
+
</template>
|
|
1151
|
+
</ui-combobox>
|
|
1152
|
+
`
|
|
1153
|
+
const combobox = root.querySelector("ui-combobox")!
|
|
1154
|
+
combobox.itemToLabel = (value) => countryLabel(value as string)
|
|
1155
|
+
const summary = root.querySelector("p")!
|
|
1156
|
+
const list = new Intl.ListFormat(document.documentElement.lang || undefined, {
|
|
1157
|
+
type: "conjunction",
|
|
1158
|
+
})
|
|
1159
|
+
const show = (value: string[]) => {
|
|
1160
|
+
const labels = value.map(countryLabel)
|
|
1161
|
+
summary.textContent = list.format(
|
|
1162
|
+
labels.length > 2
|
|
1163
|
+
? [...labels.slice(0, 2), `${labels.length - 2} more`]
|
|
1164
|
+
: labels
|
|
1165
|
+
)
|
|
1166
|
+
}
|
|
1167
|
+
show(["fr", "jp", "se", "de", "ch"])
|
|
1168
|
+
combobox.addEventListener("valuechange", (event) =>
|
|
1169
|
+
show(event.detail.value as string[])
|
|
1170
|
+
)
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export { mount }
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
Add or remove countries. Chips that no longer fit are replaced by an overflow count; the summary below lists the first two.
|
|
1177
|
+
|
|
1178
|
+
## Max Selected
|
|
1179
|
+
|
|
1180
|
+
`max-selected` limits a multiple selection. Once the limit is reached, other options become disabled; their `maxReached` state explains why.
|
|
1181
|
+
|
|
1182
|
+
```ts title="max-selected.ts"
|
|
1183
|
+
import "@ui-sdk/web/combobox"
|
|
1184
|
+
import { Check, createElement } from "lucide"
|
|
1185
|
+
|
|
1186
|
+
const labels = [
|
|
1187
|
+
"Bug",
|
|
1188
|
+
"Feature",
|
|
1189
|
+
"Documentation",
|
|
1190
|
+
"Design",
|
|
1191
|
+
"Performance",
|
|
1192
|
+
"Security",
|
|
1193
|
+
"Testing",
|
|
1194
|
+
]
|
|
1195
|
+
|
|
1196
|
+
function mount(root: HTMLElement) {
|
|
1197
|
+
root.innerHTML = `
|
|
1198
|
+
<ui-combobox multiple max-selected="3" default-value="Bug, Design">
|
|
1199
|
+
<div data-slot="stack">
|
|
1200
|
+
<ui-combobox-label>Labels (up to 3)</ui-combobox-label>
|
|
1201
|
+
<ui-combobox-chips>
|
|
1202
|
+
<ui-combobox-input>
|
|
1203
|
+
<input placeholder="Add labels…" />
|
|
1204
|
+
</ui-combobox-input>
|
|
1205
|
+
</ui-combobox-chips>
|
|
1206
|
+
</div>
|
|
1207
|
+
<ui-combobox-content hidden>
|
|
1208
|
+
<ui-combobox-list>
|
|
1209
|
+
${labels
|
|
1210
|
+
.map(
|
|
1211
|
+
(label) => `
|
|
1212
|
+
<ui-combobox-item value="${label}" label="${label}">
|
|
1213
|
+
${label}
|
|
1214
|
+
<span data-slot="hint" data-show="disabled">Limit reached</span>
|
|
1215
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
1216
|
+
</ui-combobox-item>
|
|
1217
|
+
`
|
|
1218
|
+
)
|
|
1219
|
+
.join("")}
|
|
1220
|
+
</ui-combobox-list>
|
|
1221
|
+
</ui-combobox-content>
|
|
1222
|
+
<template>
|
|
1223
|
+
<ui-combobox-chip>
|
|
1224
|
+
<ui-combobox-chip-remove><button type="button">×</button></ui-combobox-chip-remove>
|
|
1225
|
+
</ui-combobox-chip>
|
|
1226
|
+
</template>
|
|
1227
|
+
</ui-combobox>
|
|
1228
|
+
`
|
|
1229
|
+
const combobox = root.querySelector("ui-combobox")
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
export { mount }
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
Choose a third label. Other unselected labels become unavailable until you remove one.
|
|
1236
|
+
|
|
1237
|
+
## Create Option
|
|
1238
|
+
|
|
1239
|
+
`ui-combobox-create` offers the query when no label matches it exactly and it meets `min-length`. Handle its `create` event to save it; pass a promise to `event.detail.waitUntil()` for asynchronous work. Creation does not change the selection itself: add the new item and select it after saving.
|
|
1240
|
+
|
|
1241
|
+
While saving, the option reports `data-pending`, `aria-busy` and `aria-disabled`. A rejected promise is available through its error state.
|
|
1242
|
+
|
|
1243
|
+
```ts title="create.ts"
|
|
1244
|
+
import "@ui-sdk/web/combobox"
|
|
1245
|
+
import { Check, createElement, LoaderCircle, Plus } from "lucide"
|
|
1246
|
+
|
|
1247
|
+
const labels = [
|
|
1248
|
+
"Bug",
|
|
1249
|
+
"Feature",
|
|
1250
|
+
"Documentation",
|
|
1251
|
+
"Design",
|
|
1252
|
+
"Performance",
|
|
1253
|
+
"Security",
|
|
1254
|
+
"Testing",
|
|
1255
|
+
]
|
|
1256
|
+
|
|
1257
|
+
function saveLabel(name: string) {
|
|
1258
|
+
return new Promise<void>((resolve, reject) =>
|
|
1259
|
+
setTimeout(
|
|
1260
|
+
() =>
|
|
1261
|
+
name.length > 20
|
|
1262
|
+
? reject(new Error("Labels are 20 characters at most."))
|
|
1263
|
+
: resolve(),
|
|
1264
|
+
800
|
|
1265
|
+
)
|
|
1266
|
+
)
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const item = (label: string) => `
|
|
1270
|
+
<ui-combobox-item value="${label}">
|
|
1271
|
+
${label}
|
|
1272
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
1273
|
+
</ui-combobox-item>
|
|
1274
|
+
`
|
|
1275
|
+
|
|
1276
|
+
function mount(root: HTMLElement) {
|
|
1277
|
+
root.innerHTML = `
|
|
1278
|
+
<ui-combobox multiple default-value="Bug">
|
|
1279
|
+
<div data-slot="stack">
|
|
1280
|
+
<ui-combobox-label>Labels</ui-combobox-label>
|
|
1281
|
+
<ui-combobox-chips>
|
|
1282
|
+
<ui-combobox-input>
|
|
1283
|
+
<input placeholder="Add or create a label…" />
|
|
1284
|
+
</ui-combobox-input>
|
|
1285
|
+
</ui-combobox-chips>
|
|
1286
|
+
</div>
|
|
1287
|
+
<ui-combobox-content hidden>
|
|
1288
|
+
<ui-combobox-list>
|
|
1289
|
+
${labels.map(item).join("")}
|
|
1290
|
+
<ui-combobox-create min-length="2">
|
|
1291
|
+
${createElement(LoaderCircle, { "data-slot": "spinner", "data-show": "pending", "aria-hidden": "true" }).outerHTML}
|
|
1292
|
+
<span data-show="pending">Creating “<span data-query></span>”…</span>
|
|
1293
|
+
${createElement(Plus, { "data-hide": "pending", "aria-hidden": "true" }).outerHTML}
|
|
1294
|
+
<span data-slot="stack" data-hide="pending">
|
|
1295
|
+
<span>Create “<span data-query></span>”</span>
|
|
1296
|
+
<span data-error data-slot="hint"></span>
|
|
1297
|
+
</span>
|
|
1298
|
+
</ui-combobox-create>
|
|
1299
|
+
</ui-combobox-list>
|
|
1300
|
+
</ui-combobox-content>
|
|
1301
|
+
<template>
|
|
1302
|
+
<ui-combobox-chip>
|
|
1303
|
+
<ui-combobox-chip-remove><button type="button">×</button></ui-combobox-chip-remove>
|
|
1304
|
+
</ui-combobox-chip>
|
|
1305
|
+
</template>
|
|
1306
|
+
</ui-combobox>
|
|
1307
|
+
`
|
|
1308
|
+
const combobox = root.querySelector("ui-combobox") as HTMLElement & {
|
|
1309
|
+
value: string[]
|
|
1310
|
+
}
|
|
1311
|
+
const error = root.querySelector("[data-error]")!
|
|
1312
|
+
combobox.addEventListener("querychange", (event) => {
|
|
1313
|
+
const query = (event as CustomEvent<{ value: string }>).detail.value.trim()
|
|
1314
|
+
for (const span of root.querySelectorAll("[data-query]")) {
|
|
1315
|
+
span.textContent = query
|
|
1316
|
+
}
|
|
1317
|
+
})
|
|
1318
|
+
combobox.addEventListener("create", (event) => {
|
|
1319
|
+
const { value, waitUntil } = (
|
|
1320
|
+
event as CustomEvent<{
|
|
1321
|
+
value: string
|
|
1322
|
+
waitUntil: (promise: Promise<unknown>) => void
|
|
1323
|
+
}>
|
|
1324
|
+
).detail
|
|
1325
|
+
error.textContent = ""
|
|
1326
|
+
waitUntil(
|
|
1327
|
+
saveLabel(value).then(
|
|
1328
|
+
() => {
|
|
1329
|
+
root
|
|
1330
|
+
.querySelector("ui-combobox-create")!
|
|
1331
|
+
.insertAdjacentHTML("beforebegin", item(value))
|
|
1332
|
+
combobox.value = [...combobox.value, value]
|
|
1333
|
+
},
|
|
1334
|
+
(reason: Error) => {
|
|
1335
|
+
error.textContent = reason.message
|
|
1336
|
+
throw reason
|
|
1337
|
+
}
|
|
1338
|
+
)
|
|
1339
|
+
)
|
|
1340
|
+
})
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
export { mount }
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
Type a new label and choose Create. It becomes a selected chip after saving; a label longer than 20 characters shows an error.
|
|
1347
|
+
|
|
1348
|
+
## Custom Value
|
|
1349
|
+
|
|
1350
|
+
Use `allow-custom-value` when suggestions are optional, like a datalist. Unmatched text becomes the value on Enter with nothing highlighted, or when the list closes. This does not create a new item in the collection.
|
|
1351
|
+
|
|
1352
|
+
```ts title="custom-value.ts"
|
|
1353
|
+
import "@ui-sdk/web/combobox"
|
|
1354
|
+
import { Check, createElement } from "lucide"
|
|
1355
|
+
import { log } from "./log"
|
|
1356
|
+
|
|
1357
|
+
const titles = [
|
|
1358
|
+
"Designer",
|
|
1359
|
+
"Engineer",
|
|
1360
|
+
"Engineering manager",
|
|
1361
|
+
"Product manager",
|
|
1362
|
+
"Researcher",
|
|
1363
|
+
]
|
|
1364
|
+
|
|
1365
|
+
function mount(root: HTMLElement) {
|
|
1366
|
+
root.innerHTML = `
|
|
1367
|
+
<ui-combobox allow-custom-value>
|
|
1368
|
+
<div data-slot="stack">
|
|
1369
|
+
<ui-combobox-label>Job title</ui-combobox-label>
|
|
1370
|
+
<ui-combobox-input>
|
|
1371
|
+
<input placeholder="Type any title…" />
|
|
1372
|
+
</ui-combobox-input>
|
|
1373
|
+
</div>
|
|
1374
|
+
<ui-combobox-content hidden>
|
|
1375
|
+
<ui-combobox-empty>Press Enter to use “<span></span>”.</ui-combobox-empty>
|
|
1376
|
+
<ui-combobox-list>
|
|
1377
|
+
${titles
|
|
1378
|
+
.map(
|
|
1379
|
+
(title) => `
|
|
1380
|
+
<ui-combobox-item value="${title}">
|
|
1381
|
+
${title}
|
|
1382
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
1383
|
+
</ui-combobox-item>
|
|
1384
|
+
`
|
|
1385
|
+
)
|
|
1386
|
+
.join("")}
|
|
1387
|
+
</ui-combobox-list>
|
|
1388
|
+
</ui-combobox-content>
|
|
1389
|
+
</ui-combobox>
|
|
1390
|
+
`
|
|
1391
|
+
const combobox = root.querySelector("ui-combobox")!
|
|
1392
|
+
const query = root.querySelector("ui-combobox-empty span")!
|
|
1393
|
+
combobox.addEventListener(
|
|
1394
|
+
"querychange",
|
|
1395
|
+
(event) => (query.textContent = event.detail.value)
|
|
1396
|
+
)
|
|
1397
|
+
combobox.addEventListener("valuechange", (event) => {
|
|
1398
|
+
const { value, reason } = event.detail
|
|
1399
|
+
log(root, `${value ? `“${value}”` : "No title"} (${reason})`)
|
|
1400
|
+
})
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
export { mount }
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
Type a job title that isn't suggested, then press Enter without a highlighted option or leave the field. The text becomes the value, and the console logs it.
|
|
1407
|
+
|
|
1408
|
+
## Input in the Popup
|
|
1409
|
+
|
|
1410
|
+
The input can live inside the popup, opened by a button showing the current value. The button then stays in the tab order. Anchor the content to it, clear the query when opening, and return focus to it after closing, as this example does.
|
|
1411
|
+
|
|
1412
|
+
```ts title="popup.ts"
|
|
1413
|
+
import "@ui-sdk/web/combobox"
|
|
1414
|
+
import { Check, ChevronDown, createElement, Search } from "lucide"
|
|
1415
|
+
|
|
1416
|
+
const countries = [
|
|
1417
|
+
{ value: "ar", label: "Argentina" },
|
|
1418
|
+
{ value: "au", label: "Australia" },
|
|
1419
|
+
{ value: "at", label: "Austria" },
|
|
1420
|
+
{ value: "be", label: "Belgium" },
|
|
1421
|
+
{ value: "br", label: "Brazil" },
|
|
1422
|
+
{ value: "ca", label: "Canada" },
|
|
1423
|
+
{ value: "cl", label: "Chile" },
|
|
1424
|
+
{ value: "dk", label: "Denmark" },
|
|
1425
|
+
{ value: "fi", label: "Finland" },
|
|
1426
|
+
{ value: "fr", label: "France" },
|
|
1427
|
+
{ value: "de", label: "Germany" },
|
|
1428
|
+
{ value: "gr", label: "Greece" },
|
|
1429
|
+
{ value: "jp", label: "Japan" },
|
|
1430
|
+
{ value: "nl", label: "Netherlands" },
|
|
1431
|
+
{ value: "es", label: "Spain" },
|
|
1432
|
+
{ value: "se", label: "Sweden" },
|
|
1433
|
+
{ value: "ch", label: "Switzerland" },
|
|
1434
|
+
]
|
|
1435
|
+
|
|
1436
|
+
const countryLabel = (value: string) =>
|
|
1437
|
+
countries.find((country) => country.value === value)?.label ?? value
|
|
1438
|
+
|
|
1439
|
+
function mount(root: HTMLElement) {
|
|
1440
|
+
root.innerHTML = `
|
|
1441
|
+
<ui-combobox default-value="jp">
|
|
1442
|
+
<ui-combobox-trigger>
|
|
1443
|
+
<button id="combobox-popup-button" aria-label="Country">
|
|
1444
|
+
<span>Japan</span>
|
|
1445
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
1446
|
+
</button>
|
|
1447
|
+
</ui-combobox-trigger>
|
|
1448
|
+
<ui-combobox-content hidden anchor="#combobox-popup-button">
|
|
1449
|
+
<div data-slot="row">
|
|
1450
|
+
${createElement(Search, { "aria-hidden": "true" }).outerHTML}
|
|
1451
|
+
<ui-combobox-input>
|
|
1452
|
+
<input autofocus aria-label="Search countries" placeholder="Search…" />
|
|
1453
|
+
</ui-combobox-input>
|
|
1454
|
+
</div>
|
|
1455
|
+
<ui-combobox-empty>No countries found.</ui-combobox-empty>
|
|
1456
|
+
<ui-combobox-list>
|
|
1457
|
+
${countries
|
|
1458
|
+
.map(
|
|
1459
|
+
(country) => `
|
|
1460
|
+
<ui-combobox-item value="${country.value}" label="${country.label}">
|
|
1461
|
+
${country.label}
|
|
1462
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
1463
|
+
</ui-combobox-item>
|
|
1464
|
+
`
|
|
1465
|
+
)
|
|
1466
|
+
.join("")}
|
|
1467
|
+
</ui-combobox-list>
|
|
1468
|
+
</ui-combobox-content>
|
|
1469
|
+
</ui-combobox>
|
|
1470
|
+
`
|
|
1471
|
+
const combobox = root.querySelector("ui-combobox") as HTMLElement & {
|
|
1472
|
+
query: string
|
|
1473
|
+
itemToLabel: (value: string) => string
|
|
1474
|
+
}
|
|
1475
|
+
const button = root.querySelector<HTMLButtonElement>(
|
|
1476
|
+
"#combobox-popup-button"
|
|
1477
|
+
)!
|
|
1478
|
+
combobox.itemToLabel = countryLabel
|
|
1479
|
+
combobox.addEventListener("valuechange", (event) => {
|
|
1480
|
+
const { value } = (event as CustomEvent<{ value: string | null }>).detail
|
|
1481
|
+
button.querySelector("span")!.textContent = value
|
|
1482
|
+
? countryLabel(value)
|
|
1483
|
+
: "Select a country"
|
|
1484
|
+
})
|
|
1485
|
+
combobox.addEventListener("openchange", (event) => {
|
|
1486
|
+
if ((event as CustomEvent<{ value: boolean }>).detail.value) {
|
|
1487
|
+
combobox.query = ""
|
|
1488
|
+
}
|
|
1489
|
+
})
|
|
1490
|
+
combobox.addEventListener("openchangecomplete", (event) => {
|
|
1491
|
+
if (!(event as CustomEvent<{ value: boolean }>).detail.value) {
|
|
1492
|
+
button.focus()
|
|
1493
|
+
}
|
|
1494
|
+
})
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
export { mount }
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
Press Country to open the search field inside the popup. Choose a country; closing the popup returns focus to the button.
|
|
1501
|
+
|
|
1502
|
+
## Async Search
|
|
1503
|
+
|
|
1504
|
+
Use [createAsyncOptions](../utilities/async-options.md) to debounce a search and discard stale results. Pass its `items` and `status` to the combobox, set `filter` to `false`, and send query changes to the loader.
|
|
1505
|
+
|
|
1506
|
+
`ui-combobox-status` and `ui-combobox-empty` provide loading, error, and empty messages. Without a custom status, the combobox announces the result count once typing pauses.
|
|
1507
|
+
|
|
1508
|
+
```ts title="async.ts"
|
|
1509
|
+
import { createAsyncOptions } from "@ui-sdk/core/async-options"
|
|
1510
|
+
import "@ui-sdk/web/combobox"
|
|
1511
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
1512
|
+
|
|
1513
|
+
const movieTitles = [
|
|
1514
|
+
"The Shawshank Redemption",
|
|
1515
|
+
"The Godfather",
|
|
1516
|
+
"The Dark Knight",
|
|
1517
|
+
"Pulp Fiction",
|
|
1518
|
+
"Schindler's List",
|
|
1519
|
+
"The Lord of the Rings",
|
|
1520
|
+
"Fight Club",
|
|
1521
|
+
"Forrest Gump",
|
|
1522
|
+
"Inception",
|
|
1523
|
+
"The Matrix",
|
|
1524
|
+
"Goodfellas",
|
|
1525
|
+
"Interstellar",
|
|
1526
|
+
"Parasite",
|
|
1527
|
+
"Spirited Away",
|
|
1528
|
+
"Whiplash",
|
|
1529
|
+
"The Prestige",
|
|
1530
|
+
"The Departed",
|
|
1531
|
+
"Gladiator",
|
|
1532
|
+
"Alien",
|
|
1533
|
+
"Casablanca",
|
|
1534
|
+
"Amélie",
|
|
1535
|
+
"Arrival",
|
|
1536
|
+
]
|
|
1537
|
+
|
|
1538
|
+
function searchMovies(query: string, { signal }: { signal: AbortSignal }) {
|
|
1539
|
+
return new Promise<{ value: string; label: string }[]>((resolve, reject) => {
|
|
1540
|
+
const timer = setTimeout(() => {
|
|
1541
|
+
const needle = query.trim().toLowerCase()
|
|
1542
|
+
resolve(
|
|
1543
|
+
movieTitles
|
|
1544
|
+
.filter((title) => title.toLowerCase().includes(needle))
|
|
1545
|
+
.map((title) => ({
|
|
1546
|
+
value: title.toLowerCase().replace(/\W+/g, "-"),
|
|
1547
|
+
label: title,
|
|
1548
|
+
}))
|
|
1549
|
+
)
|
|
1550
|
+
}, 500)
|
|
1551
|
+
signal.addEventListener("abort", () => {
|
|
1552
|
+
clearTimeout(timer)
|
|
1553
|
+
reject(signal.reason)
|
|
1554
|
+
})
|
|
1555
|
+
})
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function mount(root: HTMLElement) {
|
|
1559
|
+
root.innerHTML = `
|
|
1560
|
+
<ui-combobox filter="false" default-query="the">
|
|
1561
|
+
<div data-slot="stack">
|
|
1562
|
+
<ui-combobox-label>Movie</ui-combobox-label>
|
|
1563
|
+
<div data-slot="row">
|
|
1564
|
+
<ui-combobox-input>
|
|
1565
|
+
<input placeholder="Search movies…" />
|
|
1566
|
+
</ui-combobox-input>
|
|
1567
|
+
<ui-combobox-trigger>
|
|
1568
|
+
<button aria-label="Show movies">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
1569
|
+
</ui-combobox-trigger>
|
|
1570
|
+
</div>
|
|
1571
|
+
</div>
|
|
1572
|
+
<ui-combobox-content hidden>
|
|
1573
|
+
<ui-combobox-status></ui-combobox-status>
|
|
1574
|
+
<ui-combobox-empty>No movies match “<span>the</span>”.</ui-combobox-empty>
|
|
1575
|
+
<ui-combobox-list></ui-combobox-list>
|
|
1576
|
+
</ui-combobox-content>
|
|
1577
|
+
<template>
|
|
1578
|
+
<ui-combobox-item>
|
|
1579
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
1580
|
+
</ui-combobox-item>
|
|
1581
|
+
</template>
|
|
1582
|
+
</ui-combobox>
|
|
1583
|
+
`
|
|
1584
|
+
const combobox = root.querySelector("ui-combobox")!
|
|
1585
|
+
const status = root.querySelector("ui-combobox-status")!
|
|
1586
|
+
const query = root.querySelector("ui-combobox-empty span")!
|
|
1587
|
+
const movies = createAsyncOptions(searchMovies, {
|
|
1588
|
+
debounce: 300,
|
|
1589
|
+
query: "the",
|
|
1590
|
+
immediate: true,
|
|
1591
|
+
})
|
|
1592
|
+
const update = () => {
|
|
1593
|
+
const { items, status: loading } = movies.getState()
|
|
1594
|
+
combobox.items = items
|
|
1595
|
+
combobox.setAttribute("status", loading)
|
|
1596
|
+
status.textContent =
|
|
1597
|
+
loading === "error"
|
|
1598
|
+
? "Couldn't load movies."
|
|
1599
|
+
: loading === "loading"
|
|
1600
|
+
? "Searching…"
|
|
1601
|
+
: `${items.length} ${items.length === 1 ? "movie" : "movies"}`
|
|
1602
|
+
}
|
|
1603
|
+
movies.subscribe(update)
|
|
1604
|
+
update()
|
|
1605
|
+
combobox.addEventListener("querychange", (event) => {
|
|
1606
|
+
query.textContent = event.detail.value
|
|
1607
|
+
movies.setQuery(event.detail.value)
|
|
1608
|
+
})
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
export { mount }
|
|
1612
|
+
```
|
|
1613
|
+
|
|
1614
|
+
Replace “the” with another movie title. The status shows the pending search, then the latest results.
|
|
1615
|
+
|
|
1616
|
+
## Load More
|
|
1617
|
+
|
|
1618
|
+
The `loadmore` event fires when scrolling or highlighting reaches the last option, once per page and not while a page loads. Connect it to the async loader’s `loadMore()`; return `{ items, hasMore }` from each page.
|
|
1619
|
+
|
|
1620
|
+
```ts title="infinite.ts"
|
|
1621
|
+
import { createAsyncOptions } from "@ui-sdk/core/async-options"
|
|
1622
|
+
import "@ui-sdk/web/combobox"
|
|
1623
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
1624
|
+
|
|
1625
|
+
const movieTitles = [
|
|
1626
|
+
"The Shawshank Redemption",
|
|
1627
|
+
"The Godfather",
|
|
1628
|
+
"The Dark Knight",
|
|
1629
|
+
"Pulp Fiction",
|
|
1630
|
+
"Schindler's List",
|
|
1631
|
+
"The Lord of the Rings",
|
|
1632
|
+
"Fight Club",
|
|
1633
|
+
"Forrest Gump",
|
|
1634
|
+
"Inception",
|
|
1635
|
+
"The Matrix",
|
|
1636
|
+
"Goodfellas",
|
|
1637
|
+
"Interstellar",
|
|
1638
|
+
"Parasite",
|
|
1639
|
+
"Spirited Away",
|
|
1640
|
+
"Whiplash",
|
|
1641
|
+
"The Prestige",
|
|
1642
|
+
"The Departed",
|
|
1643
|
+
"Gladiator",
|
|
1644
|
+
"Alien",
|
|
1645
|
+
"Casablanca",
|
|
1646
|
+
"Amélie",
|
|
1647
|
+
"Arrival",
|
|
1648
|
+
]
|
|
1649
|
+
|
|
1650
|
+
function searchMoviePages(
|
|
1651
|
+
query: string,
|
|
1652
|
+
{ signal, page }: { signal: AbortSignal; page: number }
|
|
1653
|
+
) {
|
|
1654
|
+
return new Promise<{
|
|
1655
|
+
items: { value: string; label: string }[]
|
|
1656
|
+
hasMore: boolean
|
|
1657
|
+
}>((resolve, reject) => {
|
|
1658
|
+
const timer = setTimeout(() => {
|
|
1659
|
+
const needle = query.trim().toLowerCase()
|
|
1660
|
+
const found = movieTitles
|
|
1661
|
+
.filter((title) => title.toLowerCase().includes(needle))
|
|
1662
|
+
.map((title) => ({
|
|
1663
|
+
value: title.toLowerCase().replace(/\W+/g, "-"),
|
|
1664
|
+
label: title,
|
|
1665
|
+
}))
|
|
1666
|
+
resolve({
|
|
1667
|
+
items: found.slice(page * 5, page * 5 + 5),
|
|
1668
|
+
hasMore: found.length > page * 5 + 5,
|
|
1669
|
+
})
|
|
1670
|
+
}, 500)
|
|
1671
|
+
signal.addEventListener("abort", () => {
|
|
1672
|
+
clearTimeout(timer)
|
|
1673
|
+
reject(signal.reason)
|
|
1674
|
+
})
|
|
1675
|
+
})
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function mount(root: HTMLElement) {
|
|
1679
|
+
root.innerHTML = `
|
|
1680
|
+
<ui-combobox filter="false">
|
|
1681
|
+
<div data-slot="stack">
|
|
1682
|
+
<ui-combobox-label>Movie</ui-combobox-label>
|
|
1683
|
+
<div data-slot="row">
|
|
1684
|
+
<ui-combobox-input>
|
|
1685
|
+
<input placeholder="Search movies…" />
|
|
1686
|
+
</ui-combobox-input>
|
|
1687
|
+
<ui-combobox-trigger>
|
|
1688
|
+
<button aria-label="Show movies">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
1689
|
+
</ui-combobox-trigger>
|
|
1690
|
+
</div>
|
|
1691
|
+
</div>
|
|
1692
|
+
<ui-combobox-content hidden>
|
|
1693
|
+
<ui-combobox-empty>No movies found.</ui-combobox-empty>
|
|
1694
|
+
<ui-combobox-list></ui-combobox-list>
|
|
1695
|
+
<ui-combobox-status></ui-combobox-status>
|
|
1696
|
+
</ui-combobox-content>
|
|
1697
|
+
<template>
|
|
1698
|
+
<ui-combobox-item>
|
|
1699
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
1700
|
+
</ui-combobox-item>
|
|
1701
|
+
</template>
|
|
1702
|
+
</ui-combobox>
|
|
1703
|
+
`
|
|
1704
|
+
const combobox = root.querySelector("ui-combobox")!
|
|
1705
|
+
const status = root.querySelector("ui-combobox-status")!
|
|
1706
|
+
const movies = createAsyncOptions(searchMoviePages, { immediate: true })
|
|
1707
|
+
const update = () => {
|
|
1708
|
+
const { items, status: loading, hasMore } = movies.getState()
|
|
1709
|
+
combobox.items = items
|
|
1710
|
+
combobox.setAttribute("status", loading)
|
|
1711
|
+
status.textContent =
|
|
1712
|
+
loading === "loading"
|
|
1713
|
+
? "Loading more…"
|
|
1714
|
+
: hasMore
|
|
1715
|
+
? `${items.length} loaded`
|
|
1716
|
+
: `All ${items.length}`
|
|
1717
|
+
}
|
|
1718
|
+
movies.subscribe(update)
|
|
1719
|
+
update()
|
|
1720
|
+
combobox.addEventListener("querychange", (event) =>
|
|
1721
|
+
movies.setQuery(event.detail.value)
|
|
1722
|
+
)
|
|
1723
|
+
combobox.addEventListener("loadmore", () => movies.loadMore())
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
export { mount }
|
|
1727
|
+
```
|
|
1728
|
+
|
|
1729
|
+
Open the movies and scroll or arrow to the last result. Another page loads while the existing results stay available.
|
|
1730
|
+
|
|
1731
|
+
## Virtualized
|
|
1732
|
+
|
|
1733
|
+
Supply `items` so filtering and keyboard navigation know about every option, even when only a few rows render. The `highlight` event reports the row to reveal, and each rendered item’s `index` gives its position in the full list to screen readers.
|
|
1734
|
+
|
|
1735
|
+
This example windows 10,000 items. Keep the actual row height aligned with your virtualizer’s measurement, and use the scroller’s visible height when revealing a row.
|
|
1736
|
+
|
|
1737
|
+
```ts title="virtualized.ts"
|
|
1738
|
+
import "@ui-sdk/web/combobox"
|
|
1739
|
+
import { ChevronDown, createElement } from "lucide"
|
|
1740
|
+
|
|
1741
|
+
const manyItems = Array.from({ length: 10_000 }, (_, index) => ({
|
|
1742
|
+
value: `item-${index + 1}`,
|
|
1743
|
+
label: `Item ${index + 1}`,
|
|
1744
|
+
}))
|
|
1745
|
+
|
|
1746
|
+
function visibleRows(
|
|
1747
|
+
scrollTop: number,
|
|
1748
|
+
height: number,
|
|
1749
|
+
count: number,
|
|
1750
|
+
row: number
|
|
1751
|
+
) {
|
|
1752
|
+
const start = Math.max(0, Math.floor(scrollTop / row) - 4)
|
|
1753
|
+
const end = Math.min(count, Math.ceil((scrollTop + height) / row) + 4)
|
|
1754
|
+
return { start, end }
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function scrollForRow(
|
|
1758
|
+
index: number,
|
|
1759
|
+
scrollTop: number,
|
|
1760
|
+
height: number,
|
|
1761
|
+
row: number
|
|
1762
|
+
) {
|
|
1763
|
+
const top = index * row
|
|
1764
|
+
if (top < scrollTop) {
|
|
1765
|
+
return top
|
|
1766
|
+
}
|
|
1767
|
+
if (top + row > scrollTop + height) {
|
|
1768
|
+
return top + row - height
|
|
1769
|
+
}
|
|
1770
|
+
return scrollTop
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
const row = 32
|
|
1774
|
+
const height = 288
|
|
1775
|
+
|
|
1776
|
+
function mount(root: HTMLElement) {
|
|
1777
|
+
root.innerHTML = `
|
|
1778
|
+
<ui-combobox>
|
|
1779
|
+
<div data-slot="stack">
|
|
1780
|
+
<ui-combobox-label>Item</ui-combobox-label>
|
|
1781
|
+
<div data-slot="row">
|
|
1782
|
+
<ui-combobox-input>
|
|
1783
|
+
<input placeholder="Search 10,000 items…" />
|
|
1784
|
+
</ui-combobox-input>
|
|
1785
|
+
<ui-combobox-trigger>
|
|
1786
|
+
<button aria-label="Show items">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
1787
|
+
</ui-combobox-trigger>
|
|
1788
|
+
</div>
|
|
1789
|
+
</div>
|
|
1790
|
+
<ui-combobox-content hidden>
|
|
1791
|
+
<ui-combobox-empty>No items found.</ui-combobox-empty>
|
|
1792
|
+
<div>
|
|
1793
|
+
<ui-combobox-list></ui-combobox-list>
|
|
1794
|
+
</div>
|
|
1795
|
+
</ui-combobox-content>
|
|
1796
|
+
</ui-combobox>
|
|
1797
|
+
`
|
|
1798
|
+
const combobox = root.querySelector("ui-combobox") as HTMLElement & {
|
|
1799
|
+
items: typeof manyItems
|
|
1800
|
+
}
|
|
1801
|
+
const scroller = root.querySelector<HTMLElement>("ui-combobox-content > div")!
|
|
1802
|
+
const list = root.querySelector<HTMLElement>("ui-combobox-list")!
|
|
1803
|
+
|
|
1804
|
+
let matches = manyItems
|
|
1805
|
+
const filter = (text: string) => {
|
|
1806
|
+
const needle = text.trim().toLowerCase()
|
|
1807
|
+
matches = needle
|
|
1808
|
+
? manyItems.filter((item) => item.label.toLowerCase().includes(needle))
|
|
1809
|
+
: manyItems
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
const rendered = new Map<string, HTMLElement>()
|
|
1813
|
+
const render = () => {
|
|
1814
|
+
list.style.height = `${matches.length * row}px`
|
|
1815
|
+
const { start, end } = visibleRows(
|
|
1816
|
+
scroller.scrollTop,
|
|
1817
|
+
height,
|
|
1818
|
+
matches.length,
|
|
1819
|
+
row
|
|
1820
|
+
)
|
|
1821
|
+
const visible = new Map(
|
|
1822
|
+
matches
|
|
1823
|
+
.slice(start, end)
|
|
1824
|
+
.map((item, offset) => [item.value, start + offset])
|
|
1825
|
+
)
|
|
1826
|
+
for (const [value, el] of rendered) {
|
|
1827
|
+
if (visible.has(value)) {
|
|
1828
|
+
continue
|
|
1829
|
+
}
|
|
1830
|
+
el.remove()
|
|
1831
|
+
rendered.delete(value)
|
|
1832
|
+
}
|
|
1833
|
+
for (const [value, index] of visible) {
|
|
1834
|
+
const existing = rendered.get(value)
|
|
1835
|
+
if (existing) {
|
|
1836
|
+
existing.setAttribute("index", String(index))
|
|
1837
|
+
existing.style.top = `${index * row}px`
|
|
1838
|
+
continue
|
|
1839
|
+
}
|
|
1840
|
+
const item = matches[index]
|
|
1841
|
+
list.insertAdjacentHTML(
|
|
1842
|
+
"beforeend",
|
|
1843
|
+
`<ui-combobox-item value="${item.value}" label="${item.label}" index="${index}" style="top: ${index * row}px; height: ${row}px">${item.label}</ui-combobox-item>`
|
|
1844
|
+
)
|
|
1845
|
+
rendered.set(value, list.lastElementChild as HTMLElement)
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
render()
|
|
1849
|
+
combobox.items = manyItems
|
|
1850
|
+
scroller.addEventListener("scroll", render)
|
|
1851
|
+
combobox.addEventListener("querychange", (event) => {
|
|
1852
|
+
const { value, reason } = (
|
|
1853
|
+
event as CustomEvent<{ value: string; reason: string }>
|
|
1854
|
+
).detail
|
|
1855
|
+
filter(reason === "input-change" ? value : "")
|
|
1856
|
+
render()
|
|
1857
|
+
})
|
|
1858
|
+
combobox.addEventListener("highlight", (event) => {
|
|
1859
|
+
const { index } = (
|
|
1860
|
+
event as CustomEvent<{ value: string | null; index: number }>
|
|
1861
|
+
).detail
|
|
1862
|
+
if (index < 0) {
|
|
1863
|
+
return
|
|
1864
|
+
}
|
|
1865
|
+
scroller.scrollTop = scrollForRow(
|
|
1866
|
+
index,
|
|
1867
|
+
scroller.scrollTop,
|
|
1868
|
+
scroller.clientHeight,
|
|
1869
|
+
row
|
|
1870
|
+
)
|
|
1871
|
+
render()
|
|
1872
|
+
})
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
export { mount }
|
|
1876
|
+
```
|
|
1877
|
+
|
|
1878
|
+
Search 10,000 items or move through them with the arrow keys. Only the nearby rows render.
|
|
1879
|
+
|
|
1880
|
+
## Controlled
|
|
1881
|
+
|
|
1882
|
+
The selection, text, and popup have separate values:
|
|
1883
|
+
|
|
1884
|
+
| Value | Change notification | Meaning |
|
|
1885
|
+
| ------- | ------------------- | ---------------------------------------------- |
|
|
1886
|
+
| `value` | `valuechange` | One value or `null`; an array with `multiple`. |
|
|
1887
|
+
| `query` | `querychange` | The input’s text. |
|
|
1888
|
+
| `open` | `openchange` | Whether the popup is open. |
|
|
1889
|
+
|
|
1890
|
+
Read or write the element’s properties and listen for these events. Call `preventDefault()` on a change event to reject it. Choosing an item writes its label into the input, and a canceled choice keeps the current one. Closing without choosing restores the value’s label unless custom values are allowed. [State and Events](../state-and-events.md) covers the binding pattern.
|
|
1891
|
+
|
|
1892
|
+
Focus leaving the input closes the popup with the reason `focus-out`, even when code moves it, like a notice that takes focus. To keep the popup open, call `preventDefault()` on that `openchange`.
|
|
1893
|
+
|
|
1894
|
+
```ts title="controlled.ts"
|
|
1895
|
+
import "@ui-sdk/web/combobox"
|
|
1896
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
1897
|
+
import { log } from "./log"
|
|
1898
|
+
|
|
1899
|
+
const countries = [
|
|
1900
|
+
{ value: "ar", label: "Argentina" },
|
|
1901
|
+
{ value: "au", label: "Australia" },
|
|
1902
|
+
{ value: "at", label: "Austria" },
|
|
1903
|
+
{ value: "be", label: "Belgium" },
|
|
1904
|
+
{ value: "br", label: "Brazil" },
|
|
1905
|
+
{ value: "ca", label: "Canada" },
|
|
1906
|
+
{ value: "cl", label: "Chile" },
|
|
1907
|
+
{ value: "dk", label: "Denmark" },
|
|
1908
|
+
{ value: "fi", label: "Finland" },
|
|
1909
|
+
{ value: "fr", label: "France" },
|
|
1910
|
+
{ value: "de", label: "Germany" },
|
|
1911
|
+
{ value: "gr", label: "Greece" },
|
|
1912
|
+
{ value: "jp", label: "Japan" },
|
|
1913
|
+
{ value: "nl", label: "Netherlands" },
|
|
1914
|
+
{ value: "es", label: "Spain" },
|
|
1915
|
+
{ value: "se", label: "Sweden" },
|
|
1916
|
+
{ value: "ch", label: "Switzerland" },
|
|
1917
|
+
]
|
|
1918
|
+
|
|
1919
|
+
const countryLabel = (value: string) =>
|
|
1920
|
+
countries.find((country) => country.value === value)?.label ?? value
|
|
1921
|
+
|
|
1922
|
+
function mount(root: HTMLElement) {
|
|
1923
|
+
root.innerHTML = `
|
|
1924
|
+
<div data-slot="stack">
|
|
1925
|
+
<div data-slot="controls">
|
|
1926
|
+
<button type="button" data-action="value">Choose Japan</button>
|
|
1927
|
+
<button type="button" data-action="open">Open</button>
|
|
1928
|
+
</div>
|
|
1929
|
+
<ui-combobox default-value="fr" debug>
|
|
1930
|
+
<div data-slot="stack">
|
|
1931
|
+
<ui-combobox-label>Country</ui-combobox-label>
|
|
1932
|
+
<div data-slot="row">
|
|
1933
|
+
<ui-combobox-input>
|
|
1934
|
+
<input placeholder="Search countries…" />
|
|
1935
|
+
</ui-combobox-input>
|
|
1936
|
+
<ui-combobox-trigger>
|
|
1937
|
+
<button aria-label="Show countries">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
1938
|
+
</ui-combobox-trigger>
|
|
1939
|
+
</div>
|
|
1940
|
+
</div>
|
|
1941
|
+
<ui-combobox-content hidden>
|
|
1942
|
+
<ui-combobox-empty>No countries found.</ui-combobox-empty>
|
|
1943
|
+
<ui-combobox-list>
|
|
1944
|
+
${countries
|
|
1945
|
+
.map(
|
|
1946
|
+
(country) => `
|
|
1947
|
+
<ui-combobox-item value="${country.value}" label="${country.label}">
|
|
1948
|
+
${country.label}
|
|
1949
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
1950
|
+
</ui-combobox-item>
|
|
1951
|
+
`
|
|
1952
|
+
)
|
|
1953
|
+
.join("")}
|
|
1954
|
+
</ui-combobox-list>
|
|
1955
|
+
</ui-combobox-content>
|
|
1956
|
+
</ui-combobox>
|
|
1957
|
+
</div>
|
|
1958
|
+
`
|
|
1959
|
+
const combobox = root.querySelector("ui-combobox")!
|
|
1960
|
+
combobox.itemToLabel = (value) => countryLabel(value as string)
|
|
1961
|
+
combobox.addEventListener("valuechange", (event) =>
|
|
1962
|
+
log(root, `value: ${event.detail.value} (${event.detail.reason})`)
|
|
1963
|
+
)
|
|
1964
|
+
combobox.addEventListener("querychange", (event) =>
|
|
1965
|
+
log(root, `query: “${event.detail.value}” (${event.detail.reason})`)
|
|
1966
|
+
)
|
|
1967
|
+
combobox.addEventListener("openchange", (event) =>
|
|
1968
|
+
log(root, `open: ${event.detail.value} (${event.detail.reason})`)
|
|
1969
|
+
)
|
|
1970
|
+
root
|
|
1971
|
+
.querySelector("[data-action=value]")!
|
|
1972
|
+
.addEventListener("click", () => (combobox.value = "jp"))
|
|
1973
|
+
root
|
|
1974
|
+
.querySelector("[data-action=open]")!
|
|
1975
|
+
.addEventListener("click", () => (combobox.open = true))
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
export { mount }
|
|
1979
|
+
```
|
|
1980
|
+
|
|
1981
|
+
Type, choose, or use the buttons. The console logs each value, query and open change with its reason.
|
|
1982
|
+
|
|
1983
|
+
## Controller
|
|
1984
|
+
|
|
1985
|
+
Use the element’s `api` property. It can read or set the value and query, open or close the list, and focus the input. `open()` resolves when the list closes with its reason and source, including `unmount`; pass an abort signal to close it on cancellation. [Controllers](../controllers.md) covers lifetime and setup.
|
|
1986
|
+
|
|
1987
|
+
```ts title="controller.ts"
|
|
1988
|
+
import "@ui-sdk/web/combobox"
|
|
1989
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
1990
|
+
import { log } from "./log"
|
|
1991
|
+
|
|
1992
|
+
const countries = [
|
|
1993
|
+
{ value: "ar", label: "Argentina" },
|
|
1994
|
+
{ value: "au", label: "Australia" },
|
|
1995
|
+
{ value: "at", label: "Austria" },
|
|
1996
|
+
{ value: "be", label: "Belgium" },
|
|
1997
|
+
{ value: "br", label: "Brazil" },
|
|
1998
|
+
{ value: "ca", label: "Canada" },
|
|
1999
|
+
{ value: "cl", label: "Chile" },
|
|
2000
|
+
{ value: "dk", label: "Denmark" },
|
|
2001
|
+
{ value: "fi", label: "Finland" },
|
|
2002
|
+
{ value: "fr", label: "France" },
|
|
2003
|
+
{ value: "de", label: "Germany" },
|
|
2004
|
+
{ value: "gr", label: "Greece" },
|
|
2005
|
+
{ value: "jp", label: "Japan" },
|
|
2006
|
+
{ value: "nl", label: "Netherlands" },
|
|
2007
|
+
{ value: "es", label: "Spain" },
|
|
2008
|
+
{ value: "se", label: "Sweden" },
|
|
2009
|
+
{ value: "ch", label: "Switzerland" },
|
|
2010
|
+
]
|
|
2011
|
+
|
|
2012
|
+
const countryLabel = (value: string) =>
|
|
2013
|
+
countries.find((country) => country.value === value)?.label ?? value
|
|
2014
|
+
|
|
2015
|
+
function mount(root: HTMLElement) {
|
|
2016
|
+
root.innerHTML = `
|
|
2017
|
+
<div data-slot="stack">
|
|
2018
|
+
<div data-slot="controls">
|
|
2019
|
+
<button type="button" data-action="open">Open and Wait</button>
|
|
2020
|
+
<button type="button" data-action="search">Search “sw”</button>
|
|
2021
|
+
<button type="button" data-action="germany">Choose Germany</button>
|
|
2022
|
+
<button type="button" data-action="clear">Clear</button>
|
|
2023
|
+
</div>
|
|
2024
|
+
<ui-combobox>
|
|
2025
|
+
<div data-slot="stack">
|
|
2026
|
+
<ui-combobox-label>Country</ui-combobox-label>
|
|
2027
|
+
<div data-slot="row">
|
|
2028
|
+
<ui-combobox-input>
|
|
2029
|
+
<input placeholder="Search countries…" />
|
|
2030
|
+
</ui-combobox-input>
|
|
2031
|
+
<ui-combobox-trigger>
|
|
2032
|
+
<button aria-label="Show countries">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
2033
|
+
</ui-combobox-trigger>
|
|
2034
|
+
</div>
|
|
2035
|
+
</div>
|
|
2036
|
+
<ui-combobox-content hidden>
|
|
2037
|
+
<ui-combobox-empty>No countries found.</ui-combobox-empty>
|
|
2038
|
+
<ui-combobox-list>
|
|
2039
|
+
${countries
|
|
2040
|
+
.map(
|
|
2041
|
+
(item) => `
|
|
2042
|
+
<ui-combobox-item value="${item.value}" label="${item.label}">
|
|
2043
|
+
${item.label}
|
|
2044
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
2045
|
+
</ui-combobox-item>
|
|
2046
|
+
`
|
|
2047
|
+
)
|
|
2048
|
+
.join("")}
|
|
2049
|
+
</ui-combobox-list>
|
|
2050
|
+
</ui-combobox-content>
|
|
2051
|
+
</ui-combobox>
|
|
2052
|
+
</div>
|
|
2053
|
+
`
|
|
2054
|
+
const combobox = root.querySelector("ui-combobox")!
|
|
2055
|
+
combobox.itemToLabel = (value) => countryLabel(value as string)
|
|
2056
|
+
const country = combobox.api
|
|
2057
|
+
const actions: Record<string, () => void> = {
|
|
2058
|
+
open: async () => {
|
|
2059
|
+
const { reason } = await country.open()
|
|
2060
|
+
log(root, `Closed by ${reason}, with ${country.getValue() ?? "nothing"}`)
|
|
2061
|
+
},
|
|
2062
|
+
search: () => {
|
|
2063
|
+
country.focus()
|
|
2064
|
+
country.setQuery("sw")
|
|
2065
|
+
country.open()
|
|
2066
|
+
},
|
|
2067
|
+
germany: () => country.setValue("de"),
|
|
2068
|
+
clear: () => country.clear(),
|
|
2069
|
+
}
|
|
2070
|
+
for (const button of root.querySelectorAll<HTMLElement>("[data-action]")) {
|
|
2071
|
+
button.addEventListener("click", () => actions[button.dataset.action!]())
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
export { mount }
|
|
2076
|
+
```
|
|
2077
|
+
|
|
2078
|
+
Use the buttons to drive the combobox from outside. Open and Wait logs how the list closed and what was chosen.
|
|
2079
|
+
|
|
2080
|
+
## Positioning
|
|
2081
|
+
|
|
2082
|
+
`side` and `align` place the popup around the input or chips. `anchor` can use a different element, and `ui-combobox-arrow` follows the side chosen after collision handling. [Popups](../popups.md#place-the-content) covers offsets and available space.
|
|
2083
|
+
|
|
2084
|
+
The page stays usable by default. `modal="pointer"` makes an outside press only dismiss the list; `modal` also locks scrolling and makes the rest of the page inert. The popup uses the top layer and renders in place; use `container` when its DOM belongs elsewhere, such as a shadow root.
|
|
2085
|
+
|
|
2086
|
+
```ts title="positioning.ts"
|
|
2087
|
+
import "@ui-sdk/web/checkbox"
|
|
2088
|
+
import "@ui-sdk/web/combobox"
|
|
2089
|
+
import "@ui-sdk/web/select"
|
|
2090
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
2091
|
+
|
|
2092
|
+
const frameworks = [
|
|
2093
|
+
{ value: "next", label: "Next.js" },
|
|
2094
|
+
{ value: "sveltekit", label: "SvelteKit" },
|
|
2095
|
+
{ value: "nuxt", label: "Nuxt" },
|
|
2096
|
+
{ value: "remix", label: "Remix" },
|
|
2097
|
+
{ value: "astro", label: "Astro" },
|
|
2098
|
+
{ value: "solidstart", label: "SolidStart" },
|
|
2099
|
+
{ value: "qwik", label: "Qwik City" },
|
|
2100
|
+
]
|
|
2101
|
+
|
|
2102
|
+
const sides = [
|
|
2103
|
+
{ value: "top", label: "Top" },
|
|
2104
|
+
{ value: "right", label: "Right" },
|
|
2105
|
+
{ value: "bottom", label: "Bottom" },
|
|
2106
|
+
{ value: "left", label: "Left" },
|
|
2107
|
+
]
|
|
2108
|
+
const aligns = [
|
|
2109
|
+
{ value: "start", label: "Start" },
|
|
2110
|
+
{ value: "center", label: "Center" },
|
|
2111
|
+
{ value: "end", label: "End" },
|
|
2112
|
+
]
|
|
2113
|
+
|
|
2114
|
+
const select = (
|
|
2115
|
+
name: string,
|
|
2116
|
+
label: string,
|
|
2117
|
+
options: { value: string; label: string }[],
|
|
2118
|
+
value: string
|
|
2119
|
+
) => `
|
|
2120
|
+
<ui-select data-control="${name}" default-value="${value}">
|
|
2121
|
+
<ui-select-label>${label}</ui-select-label>
|
|
2122
|
+
<ui-select-trigger>
|
|
2123
|
+
<button>
|
|
2124
|
+
<ui-select-value></ui-select-value>
|
|
2125
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
2126
|
+
</button>
|
|
2127
|
+
</ui-select-trigger>
|
|
2128
|
+
<ui-select-content hidden>
|
|
2129
|
+
${options
|
|
2130
|
+
.map(
|
|
2131
|
+
(option) => `
|
|
2132
|
+
<ui-select-item value="${option.value}">
|
|
2133
|
+
${option.label}
|
|
2134
|
+
<ui-select-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-select-item-indicator>
|
|
2135
|
+
</ui-select-item>
|
|
2136
|
+
`
|
|
2137
|
+
)
|
|
2138
|
+
.join("")}
|
|
2139
|
+
</ui-select-content>
|
|
2140
|
+
</ui-select>
|
|
2141
|
+
`
|
|
2142
|
+
|
|
2143
|
+
function mount(root: HTMLElement) {
|
|
2144
|
+
root.innerHTML = `
|
|
2145
|
+
<div data-slot="stack">
|
|
2146
|
+
<div data-slot="controls">
|
|
2147
|
+
${select("side", "Side", sides, "bottom")}
|
|
2148
|
+
${select("align", "Align", aligns, "start")}
|
|
2149
|
+
<label data-slot="label">
|
|
2150
|
+
<ui-checkbox>
|
|
2151
|
+
<ui-checkbox-indicator>
|
|
2152
|
+
${createElement(Check, { "aria-hidden": "true" }).outerHTML}
|
|
2153
|
+
</ui-checkbox-indicator>
|
|
2154
|
+
</ui-checkbox>
|
|
2155
|
+
Anchor to the box
|
|
2156
|
+
</label>
|
|
2157
|
+
</div>
|
|
2158
|
+
<div id="combobox-positioning-box" data-slot="card">
|
|
2159
|
+
<ui-combobox>
|
|
2160
|
+
<ui-combobox-input>
|
|
2161
|
+
<input aria-label="Framework" placeholder="Framework" />
|
|
2162
|
+
</ui-combobox-input>
|
|
2163
|
+
<ui-combobox-content hidden side="bottom" align="start" side-offset="10" align-offset="0" collision-padding="16">
|
|
2164
|
+
<ui-combobox-empty>No frameworks found.</ui-combobox-empty>
|
|
2165
|
+
<ui-combobox-list>
|
|
2166
|
+
${frameworks
|
|
2167
|
+
.map(
|
|
2168
|
+
(framework) => `
|
|
2169
|
+
<ui-combobox-item value="${framework.value}" label="${framework.label}">
|
|
2170
|
+
${framework.label}
|
|
2171
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
2172
|
+
</ui-combobox-item>
|
|
2173
|
+
`
|
|
2174
|
+
)
|
|
2175
|
+
.join("")}
|
|
2176
|
+
</ui-combobox-list>
|
|
2177
|
+
</ui-combobox-content>
|
|
2178
|
+
</ui-combobox>
|
|
2179
|
+
</div>
|
|
2180
|
+
</div>
|
|
2181
|
+
`
|
|
2182
|
+
const content = root.querySelector("ui-combobox-content")!
|
|
2183
|
+
for (const control of root.querySelectorAll("ui-select")) {
|
|
2184
|
+
control.addEventListener("valuechange", (event) => {
|
|
2185
|
+
if (event.detail.value) {
|
|
2186
|
+
content.setAttribute(
|
|
2187
|
+
control.dataset.control!,
|
|
2188
|
+
String(event.detail.value)
|
|
2189
|
+
)
|
|
2190
|
+
}
|
|
2191
|
+
})
|
|
2192
|
+
}
|
|
2193
|
+
root
|
|
2194
|
+
.querySelector("ui-checkbox")!
|
|
2195
|
+
.addEventListener("checkedchange", (event) => {
|
|
2196
|
+
if (event.detail.value === true) {
|
|
2197
|
+
content.setAttribute("anchor", "#combobox-positioning-box")
|
|
2198
|
+
} else {
|
|
2199
|
+
content.removeAttribute("anchor")
|
|
2200
|
+
}
|
|
2201
|
+
})
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
export { mount }
|
|
2205
|
+
```
|
|
2206
|
+
|
|
2207
|
+
Pick a side and an alignment, then open the list. Check Anchor to the Box to place it against the box instead of the input.
|
|
2208
|
+
|
|
2209
|
+
## Animation
|
|
2210
|
+
|
|
2211
|
+
The content has `data-starting-style` while it opens and `data-ending-style` while it closes. Transition from those states, and the popup stays mounted until the exit finishes. The `openchangecomplete` event fires after opening or closing finishes, and `keep-mounted` on an item indicator keeps it in the page for a selection animation.
|
|
2212
|
+
|
|
2213
|
+
```css
|
|
2214
|
+
[data-slot="combobox-content"] {
|
|
2215
|
+
transition:
|
|
2216
|
+
opacity 0.15s,
|
|
2217
|
+
translate 0.2s cubic-bezier(0.22, 1, 0.36, 1);
|
|
2218
|
+
|
|
2219
|
+
&:is([data-starting-style], [data-ending-style]) {
|
|
2220
|
+
opacity: 0;
|
|
2221
|
+
translate: 0 -6px;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
```
|
|
2225
|
+
|
|
2226
|
+
```ts title="animation.ts"
|
|
2227
|
+
import "@ui-sdk/web/combobox"
|
|
2228
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
2229
|
+
import { log } from "./log"
|
|
2230
|
+
|
|
2231
|
+
const frameworks = [
|
|
2232
|
+
{ value: "next", label: "Next.js" },
|
|
2233
|
+
{ value: "sveltekit", label: "SvelteKit" },
|
|
2234
|
+
{ value: "nuxt", label: "Nuxt" },
|
|
2235
|
+
{ value: "remix", label: "Remix" },
|
|
2236
|
+
{ value: "astro", label: "Astro" },
|
|
2237
|
+
{ value: "solidstart", label: "SolidStart" },
|
|
2238
|
+
{ value: "qwik", label: "Qwik City" },
|
|
2239
|
+
]
|
|
2240
|
+
|
|
2241
|
+
function mount(root: HTMLElement) {
|
|
2242
|
+
root.innerHTML = `
|
|
2243
|
+
<ui-combobox>
|
|
2244
|
+
<div data-slot="stack">
|
|
2245
|
+
<ui-combobox-label>Framework</ui-combobox-label>
|
|
2246
|
+
<div data-slot="row">
|
|
2247
|
+
<ui-combobox-input>
|
|
2248
|
+
<input placeholder="Select a framework" />
|
|
2249
|
+
</ui-combobox-input>
|
|
2250
|
+
<ui-combobox-trigger>
|
|
2251
|
+
<button aria-label="Show frameworks">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
2252
|
+
</ui-combobox-trigger>
|
|
2253
|
+
</div>
|
|
2254
|
+
</div>
|
|
2255
|
+
<ui-combobox-content hidden>
|
|
2256
|
+
<ui-combobox-empty>No frameworks found.</ui-combobox-empty>
|
|
2257
|
+
<ui-combobox-list>
|
|
2258
|
+
${frameworks
|
|
2259
|
+
.map(
|
|
2260
|
+
(framework) => `
|
|
2261
|
+
<ui-combobox-item value="${framework.value}" label="${framework.label}">
|
|
2262
|
+
${framework.label}
|
|
2263
|
+
<ui-combobox-item-indicator keep-mounted>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
2264
|
+
</ui-combobox-item>
|
|
2265
|
+
`
|
|
2266
|
+
)
|
|
2267
|
+
.join("")}
|
|
2268
|
+
</ui-combobox-list>
|
|
2269
|
+
</ui-combobox-content>
|
|
2270
|
+
</ui-combobox>
|
|
2271
|
+
`
|
|
2272
|
+
root
|
|
2273
|
+
.querySelector("ui-combobox")!
|
|
2274
|
+
.addEventListener("openchangecomplete", (event) =>
|
|
2275
|
+
log(root, event.detail.value ? "Finished opening" : "Finished closing")
|
|
2276
|
+
)
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
export { mount }
|
|
2280
|
+
```
|
|
2281
|
+
|
|
2282
|
+
Open and close the list. The console logs when each animation finishes.
|
|
2283
|
+
|
|
2284
|
+
## Presentation
|
|
2285
|
+
|
|
2286
|
+
Use `presentation="sheet"` for a sheet, or `"auto"` to choose a sheet on small touch screens each time the list opens. The input moves into the sheet’s header; there is still one input and one query.
|
|
2287
|
+
|
|
2288
|
+
A touch opening leaves the keyboard closed. Tapping the header input opens the keyboard and grows the sheet; closing the keyboard keeps the query. Choosing an item closes the sheet unless `multiple` is set. Swiping down closes it with the reason `swipe`.
|
|
2289
|
+
|
|
2290
|
+
Keep the options inside `ui-combobox-list` so the header stays outside the listbox. Give sheet content a height, style it with `data-presentation`, and leave its `translate` for the sheet behavior. `--sheet-backdrop` controls the backdrop. The sheet is modal unless you set `modal` to false; mouse and keyboard openings keep input focus as it moves.
|
|
2291
|
+
|
|
2292
|
+
```ts title="presentation.ts"
|
|
2293
|
+
import "@ui-sdk/web/combobox"
|
|
2294
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
2295
|
+
|
|
2296
|
+
const frameworks = [
|
|
2297
|
+
{ value: "next", label: "Next.js" },
|
|
2298
|
+
{ value: "sveltekit", label: "SvelteKit" },
|
|
2299
|
+
{ value: "nuxt", label: "Nuxt" },
|
|
2300
|
+
{ value: "remix", label: "Remix" },
|
|
2301
|
+
{ value: "astro", label: "Astro" },
|
|
2302
|
+
{ value: "solidstart", label: "SolidStart" },
|
|
2303
|
+
{ value: "qwik", label: "Qwik City" },
|
|
2304
|
+
]
|
|
2305
|
+
|
|
2306
|
+
function mount(root: HTMLElement) {
|
|
2307
|
+
root.innerHTML = `
|
|
2308
|
+
<ui-combobox presentation="sheet">
|
|
2309
|
+
<div data-slot="stack">
|
|
2310
|
+
<ui-combobox-label>Framework</ui-combobox-label>
|
|
2311
|
+
<div data-slot="row">
|
|
2312
|
+
<ui-combobox-input>
|
|
2313
|
+
<input placeholder="Select a framework" />
|
|
2314
|
+
</ui-combobox-input>
|
|
2315
|
+
<ui-combobox-trigger>
|
|
2316
|
+
<button aria-label="Show frameworks">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
2317
|
+
</ui-combobox-trigger>
|
|
2318
|
+
</div>
|
|
2319
|
+
</div>
|
|
2320
|
+
<ui-combobox-content hidden>
|
|
2321
|
+
<ui-combobox-empty>No frameworks found.</ui-combobox-empty>
|
|
2322
|
+
<ui-combobox-list>
|
|
2323
|
+
${frameworks
|
|
2324
|
+
.map(
|
|
2325
|
+
(framework) => `
|
|
2326
|
+
<ui-combobox-item value="${framework.value}" label="${framework.label}">
|
|
2327
|
+
${framework.label}
|
|
2328
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
2329
|
+
</ui-combobox-item>
|
|
2330
|
+
`
|
|
2331
|
+
)
|
|
2332
|
+
.join("")}
|
|
2333
|
+
</ui-combobox-list>
|
|
2334
|
+
</ui-combobox-content>
|
|
2335
|
+
</ui-combobox>
|
|
2336
|
+
`
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
export { mount }
|
|
2340
|
+
```
|
|
2341
|
+
|
|
2342
|
+
Open the field to see a sheet. On touch, tap the search field in its header to bring up the keyboard, or swipe down to close it.
|
|
2343
|
+
|
|
2344
|
+
## Forms
|
|
2345
|
+
|
|
2346
|
+
A combobox submits the selected value under `name`, rather than the query text. Multiple values submit as separate entries. `required` prevents empty submissions, reset restores the default selection and its label, and `form` joins a form from elsewhere on the page.
|
|
2347
|
+
|
|
2348
|
+
The custom element joins the form once its definition loads. [Forms](../forms.md) covers the shared integration.
|
|
2349
|
+
|
|
2350
|
+
In a [`ui-field`](./field.md), the field supplies the input's accessible name, description and error. Submitting a required field without a selection sets `aria-invalid` and `data-invalid`.
|
|
2351
|
+
|
|
2352
|
+
```ts title="form.ts"
|
|
2353
|
+
import "@ui-sdk/web/combobox"
|
|
2354
|
+
import "@ui-sdk/web/field"
|
|
2355
|
+
import { Check, ChevronDown, createElement, X } from "lucide"
|
|
2356
|
+
import { log } from "./log"
|
|
2357
|
+
|
|
2358
|
+
const countries = [
|
|
2359
|
+
{ value: "ar", label: "Argentina" },
|
|
2360
|
+
{ value: "au", label: "Australia" },
|
|
2361
|
+
{ value: "at", label: "Austria" },
|
|
2362
|
+
{ value: "be", label: "Belgium" },
|
|
2363
|
+
{ value: "br", label: "Brazil" },
|
|
2364
|
+
{ value: "ca", label: "Canada" },
|
|
2365
|
+
{ value: "cl", label: "Chile" },
|
|
2366
|
+
{ value: "dk", label: "Denmark" },
|
|
2367
|
+
{ value: "fi", label: "Finland" },
|
|
2368
|
+
{ value: "fr", label: "France" },
|
|
2369
|
+
{ value: "de", label: "Germany" },
|
|
2370
|
+
{ value: "gr", label: "Greece" },
|
|
2371
|
+
{ value: "jp", label: "Japan" },
|
|
2372
|
+
{ value: "nl", label: "Netherlands" },
|
|
2373
|
+
{ value: "es", label: "Spain" },
|
|
2374
|
+
{ value: "se", label: "Sweden" },
|
|
2375
|
+
{ value: "ch", label: "Switzerland" },
|
|
2376
|
+
]
|
|
2377
|
+
|
|
2378
|
+
const countryLabel = (value: string) =>
|
|
2379
|
+
countries.find((country) => country.value === value)?.label ?? value
|
|
2380
|
+
|
|
2381
|
+
const interests = ["Design", "Engineering", "Marketing", "Research", "Sales"]
|
|
2382
|
+
|
|
2383
|
+
function mount(root: HTMLElement) {
|
|
2384
|
+
root.innerHTML = `
|
|
2385
|
+
<form data-slot="stack">
|
|
2386
|
+
<ui-field name="country">
|
|
2387
|
+
<ui-field-label>Country</ui-field-label>
|
|
2388
|
+
<ui-combobox required>
|
|
2389
|
+
<div data-slot="row">
|
|
2390
|
+
<ui-combobox-input>
|
|
2391
|
+
<input placeholder="Search countries…" />
|
|
2392
|
+
</ui-combobox-input>
|
|
2393
|
+
<ui-combobox-trigger>
|
|
2394
|
+
<button aria-label="Show countries">${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}</button>
|
|
2395
|
+
</ui-combobox-trigger>
|
|
2396
|
+
</div>
|
|
2397
|
+
<ui-combobox-content hidden>
|
|
2398
|
+
<ui-combobox-empty>No countries found.</ui-combobox-empty>
|
|
2399
|
+
<ui-combobox-list>
|
|
2400
|
+
${countries
|
|
2401
|
+
.map(
|
|
2402
|
+
(country) => `
|
|
2403
|
+
<ui-combobox-item value="${country.value}" label="${country.label}">
|
|
2404
|
+
${country.label}
|
|
2405
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
2406
|
+
</ui-combobox-item>
|
|
2407
|
+
`
|
|
2408
|
+
)
|
|
2409
|
+
.join("")}
|
|
2410
|
+
</ui-combobox-list>
|
|
2411
|
+
</ui-combobox-content>
|
|
2412
|
+
</ui-combobox>
|
|
2413
|
+
<ui-field-description>Where we ship your order.</ui-field-description>
|
|
2414
|
+
<div>
|
|
2415
|
+
<ui-field-error match="valueMissing">Choose a country.</ui-field-error>
|
|
2416
|
+
</div>
|
|
2417
|
+
</ui-field>
|
|
2418
|
+
<ui-field name="interests">
|
|
2419
|
+
<ui-field-label>Interests</ui-field-label>
|
|
2420
|
+
<ui-combobox multiple default-value="Design">
|
|
2421
|
+
<ui-combobox-chips>
|
|
2422
|
+
<ui-combobox-input>
|
|
2423
|
+
<input placeholder="Add interests…" />
|
|
2424
|
+
</ui-combobox-input>
|
|
2425
|
+
</ui-combobox-chips>
|
|
2426
|
+
<ui-combobox-content hidden>
|
|
2427
|
+
<ui-combobox-list>
|
|
2428
|
+
${interests
|
|
2429
|
+
.map(
|
|
2430
|
+
(interest) => `
|
|
2431
|
+
<ui-combobox-item value="${interest}">
|
|
2432
|
+
${interest}
|
|
2433
|
+
<ui-combobox-item-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-combobox-item-indicator>
|
|
2434
|
+
</ui-combobox-item>
|
|
2435
|
+
`
|
|
2436
|
+
)
|
|
2437
|
+
.join("")}
|
|
2438
|
+
</ui-combobox-list>
|
|
2439
|
+
</ui-combobox-content>
|
|
2440
|
+
<template>
|
|
2441
|
+
<ui-combobox-chip>
|
|
2442
|
+
<ui-combobox-chip-remove>
|
|
2443
|
+
<button type="button">${createElement(X, { "aria-hidden": "true" }).outerHTML}</button>
|
|
2444
|
+
</ui-combobox-chip-remove>
|
|
2445
|
+
</ui-combobox-chip>
|
|
2446
|
+
</template>
|
|
2447
|
+
</ui-combobox>
|
|
2448
|
+
</ui-field>
|
|
2449
|
+
<div data-slot="row">
|
|
2450
|
+
<button type="submit">Submit</button>
|
|
2451
|
+
<button type="reset">Reset</button>
|
|
2452
|
+
</div>
|
|
2453
|
+
</form>
|
|
2454
|
+
`
|
|
2455
|
+
root.querySelector("ui-combobox")!.itemToLabel = (value) =>
|
|
2456
|
+
countryLabel(value as string)
|
|
2457
|
+
const form = root.querySelector("form")!
|
|
2458
|
+
form.addEventListener("submit", (event) => {
|
|
2459
|
+
event.preventDefault()
|
|
2460
|
+
const entries = [...new FormData(form)].map(
|
|
2461
|
+
([name, value]) => `${name}=${value}`
|
|
2462
|
+
)
|
|
2463
|
+
log(root, `Submitted ${entries.join("&")}`)
|
|
2464
|
+
})
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
export { mount }
|
|
2468
|
+
```
|
|
2469
|
+
|
|
2470
|
+
Submit without a country to see the inline error. Choose one and submit; the console logs the values, and Reset restores the defaults.
|
|
2471
|
+
|
|
2472
|
+
## Custom Rendering
|
|
2473
|
+
|
|
2474
|
+
Elements have no render functions. Put your own markup around and inside them, style them from their attributes, and watch those attributes to change what's inside: here the trigger's button swaps its chevron on `data-open`, and each item shows a check or Enter from `data-selected` and `data-highlighted`. Each element's state is listed [below](#state).
|
|
2475
|
+
|
|
2476
|
+
```ts title="custom-rendering.ts"
|
|
2477
|
+
import "@ui-sdk/web/combobox"
|
|
2478
|
+
import { Check, ChevronDown, ChevronUp, createElement, Search } from "lucide"
|
|
2479
|
+
|
|
2480
|
+
const users = [
|
|
2481
|
+
{ id: 1, name: "Hollis Grant", username: "hollis", role: "Owner" },
|
|
2482
|
+
{ id: 2, name: "Inès Moreau", username: "ines", role: "Admin" },
|
|
2483
|
+
{ id: 3, name: "Kofi Mensah", username: "kofi", role: "Member" },
|
|
2484
|
+
{ id: 4, name: "Wren Takahashi", username: "wren", role: "Member" },
|
|
2485
|
+
{ id: 5, name: "Tomasz Wójcik", username: "tomasz", role: "Guest" },
|
|
2486
|
+
]
|
|
2487
|
+
|
|
2488
|
+
const chevron = (open: boolean) =>
|
|
2489
|
+
createElement(open ? ChevronUp : ChevronDown, { "aria-hidden": "true" })
|
|
2490
|
+
.outerHTML
|
|
2491
|
+
|
|
2492
|
+
const mark = (item: Element) =>
|
|
2493
|
+
item.hasAttribute("data-selected")
|
|
2494
|
+
? createElement(Check, { "aria-hidden": "true" }).outerHTML
|
|
2495
|
+
: item.hasAttribute("data-highlighted")
|
|
2496
|
+
? `<kbd data-slot="kbd">Enter</kbd>`
|
|
2497
|
+
: ""
|
|
2498
|
+
|
|
2499
|
+
function mount(root: HTMLElement) {
|
|
2500
|
+
root.innerHTML = `
|
|
2501
|
+
<ui-combobox>
|
|
2502
|
+
<div data-slot="stack">
|
|
2503
|
+
<ui-combobox-label>Assignee</ui-combobox-label>
|
|
2504
|
+
<div data-slot="row">
|
|
2505
|
+
<div data-slot="row">
|
|
2506
|
+
${createElement(Search, { "aria-hidden": "true" }).outerHTML}
|
|
2507
|
+
<ui-combobox-input>
|
|
2508
|
+
<input placeholder="Search people…" />
|
|
2509
|
+
</ui-combobox-input>
|
|
2510
|
+
</div>
|
|
2511
|
+
<ui-combobox-trigger>
|
|
2512
|
+
<button aria-label="Show people">${chevron(false)}</button>
|
|
2513
|
+
</ui-combobox-trigger>
|
|
2514
|
+
</div>
|
|
2515
|
+
</div>
|
|
2516
|
+
<ui-combobox-content hidden>
|
|
2517
|
+
<ui-combobox-empty>No one by that name.</ui-combobox-empty>
|
|
2518
|
+
<ui-combobox-list>
|
|
2519
|
+
${users
|
|
2520
|
+
.map(
|
|
2521
|
+
(user) => `
|
|
2522
|
+
<ui-combobox-item value="${user.username}" label="${user.name}" keywords="${user.username}">
|
|
2523
|
+
<span data-slot="badge">${user.name.slice(0, 1)}</span>
|
|
2524
|
+
<span data-slot="stack">
|
|
2525
|
+
${user.name}
|
|
2526
|
+
<span data-slot="hint">@${user.username}</span>
|
|
2527
|
+
</span>
|
|
2528
|
+
</ui-combobox-item>
|
|
2529
|
+
`
|
|
2530
|
+
)
|
|
2531
|
+
.join("")}
|
|
2532
|
+
</ui-combobox-list>
|
|
2533
|
+
</ui-combobox-content>
|
|
2534
|
+
</ui-combobox>
|
|
2535
|
+
`
|
|
2536
|
+
const update = (el: Element) => {
|
|
2537
|
+
if (el.localName === "button") {
|
|
2538
|
+
el.innerHTML = chevron(el.hasAttribute("data-open"))
|
|
2539
|
+
}
|
|
2540
|
+
if (el.localName !== "ui-combobox-item") {
|
|
2541
|
+
return
|
|
2542
|
+
}
|
|
2543
|
+
el.querySelector(":scope > svg, :scope > kbd")?.remove()
|
|
2544
|
+
el.insertAdjacentHTML("beforeend", mark(el))
|
|
2545
|
+
}
|
|
2546
|
+
const observer = new MutationObserver((records) =>
|
|
2547
|
+
records.forEach((record) => update(record.target as Element))
|
|
2548
|
+
)
|
|
2549
|
+
observer.observe(root.querySelector("ui-combobox-trigger")!, {
|
|
2550
|
+
subtree: true,
|
|
2551
|
+
attributeFilter: ["data-open"],
|
|
2552
|
+
})
|
|
2553
|
+
observer.observe(root.querySelector("ui-combobox-list")!, {
|
|
2554
|
+
subtree: true,
|
|
2555
|
+
attributeFilter: ["data-selected", "data-highlighted"],
|
|
2556
|
+
})
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
export { mount }
|
|
2560
|
+
```
|
|
2561
|
+
|
|
2562
|
+
Open the list and move through it. The chevron flips while it's open, the highlighted person shows Enter, and the chosen one shows a check.
|
|
2563
|
+
|
|
2564
|
+
## Styling
|
|
2565
|
+
|
|
2566
|
+
Each element has a `data-slot`, such as `combobox-content`, and shows its state through the attributes listed under [Data Attributes](#data-attributes).
|
|
2567
|
+
|
|
2568
|
+
Use `data-highlighted` for the active option and `data-selected` for chosen values. Because the keyboard highlight usually moves without focus leaving the input, use the content's `data-modality="keyboard"` to style it. Real focus can move into the list for touch screen readers or into the sheet.
|
|
2569
|
+
|
|
2570
|
+
`--anchor-width` follows the input or chips. `--available-height` doesn't inherit: when a nested scroller needs it, copy it to a custom property on the content, such as `--list-height: var(--available-height)`, then use that property on the scroller.
|
|
2571
|
+
|
|
2572
|
+
### `<ui-combobox-content>`
|
|
2573
|
+
- `--anchor-width`: Width of the anchor, usually the trigger. Use it to match widths.
|
|
2574
|
+
- `--anchor-height`: Height of the anchor.
|
|
2575
|
+
- `--available-width`: Space beside the popup, up to the viewport edge. Not inherited either.
|
|
2576
|
+
- `--available-height`: Space between the anchor and the viewport edge. Use it for max-height. Not inherited, so a child that needs it copies it on the popup, like `[--list-height:var(--available-height)]`, and reads `max-h-(--list-height)`. The first popup registers both names for the whole page with `CSS.registerProperty`, so they don't inherit anywhere. A page that needs them to inherit calls it first, with `inherits: true`. An `@property` rule can't, since a registration in script wins.
|
|
2577
|
+
- `--transform-origin`: The point on the anchor the popup opens from. Use it to scale from the right place.
|
|
2578
|
+
- `--arrow-offset`: Where the anchor's center falls along the popup's edge, kept `arrowPadding` from the corners. Position a ::before arrow with it.
|
|
2579
|
+
- `--drawer-progress`: While it presents as a sheet: how far open the sheet is, from 0 closed to 1 at its largest snap point. Never past 1.
|
|
2580
|
+
- `--drawer-dim`: While it presents as a sheet: how dim the page behind is, from 0 to 1. The backdrop's opacity follows it.
|
|
2581
|
+
- `--drawer-expand`: While it presents as a sheet: 0 at the second-largest snap point and below, 1 at the largest. With one snap point, the whole travel.
|
|
2582
|
+
- `--drawer-stack`: While it presents as a sheet: how many sheets opened from inside this one are open above it, rising smoothly as each one opens. Push it back with it.
|
|
2583
|
+
- `--sheet-backdrop`: Set it yourself: the color the page dims to while it presents as a sheet. Black at 0.2 by default.
|
|
2584
|
+
|
|
2585
|
+
### `<ui-combobox-viewport>`
|
|
2586
|
+
- `--keyboard-inset`: While it presents as a sheet: how many pixels of the page the on-screen keyboard covers, 0 without one.
|
|
2587
|
+
- `--visual-viewport-width`: While it presents as a sheet: the width of the part of the page you can see.
|
|
2588
|
+
- `--visual-viewport-height`: While it presents as a sheet: the height of the part of the page you can see, above the keyboard.
|
|
2589
|
+
- `--visual-viewport-top`: While it presents as a sheet: how far the part of the page you can see is scrolled from the top of the layout, as when the keyboard pushes it up.
|
|
2590
|
+
|
|
2591
|
+
## Data Attributes
|
|
2592
|
+
|
|
2593
|
+
### `<ui-combobox-label>`
|
|
2594
|
+
`[data-slot="combobox-label"]`
|
|
2595
|
+
|
|
2596
|
+
- `data-disabled`: Present when the combobox is disabled, by its own prop, a field or a `<fieldset>`.
|
|
2597
|
+
- `data-readonly`: Present when the combobox is read-only.
|
|
2598
|
+
- `data-required`: Present when the combobox is required.
|
|
2599
|
+
|
|
2600
|
+
### `<ui-combobox-input>`
|
|
2601
|
+
`[data-slot="combobox-input"]`
|
|
2602
|
+
|
|
2603
|
+
- `data-closed`: Present while the list is closed, including while it animates out.
|
|
2604
|
+
- `data-open`: Present while the list is open.
|
|
2605
|
+
- `data-disabled`: Present when the combobox is disabled, by its own prop, a field or a `<fieldset>`.
|
|
2606
|
+
- `data-readonly`: Present when the combobox is read-only.
|
|
2607
|
+
- `data-required`: Present when the combobox is required.
|
|
2608
|
+
|
|
2609
|
+
### `<ui-combobox-trigger>`
|
|
2610
|
+
`[data-slot="combobox-trigger"]`
|
|
2611
|
+
|
|
2612
|
+
- `data-closed`: Present while the list is closed, including while it animates out.
|
|
2613
|
+
- `data-open`: Present while the list is open.
|
|
2614
|
+
- `data-disabled`: Present when the combobox is disabled, by its own prop, a field or a `<fieldset>`.
|
|
2615
|
+
- `data-readonly`: Present when the combobox is read-only.
|
|
2616
|
+
- `data-required`: Present when the combobox is required.
|
|
2617
|
+
|
|
2618
|
+
### `<ui-combobox-clear>`
|
|
2619
|
+
`[data-slot="combobox-clear"]`
|
|
2620
|
+
|
|
2621
|
+
- `data-disabled`: Present when the combobox is disabled or read-only.
|
|
2622
|
+
|
|
2623
|
+
### `<ui-combobox-chips>`
|
|
2624
|
+
`[data-slot="combobox-chips"]`
|
|
2625
|
+
|
|
2626
|
+
- `data-disabled`: Present when the combobox is disabled, by its own prop, a field or a `<fieldset>`.
|
|
2627
|
+
- `data-readonly`: Present when the combobox is read-only.
|
|
2628
|
+
- `data-required`: Present when the combobox is required.
|
|
2629
|
+
|
|
2630
|
+
### `<ui-combobox-content>`
|
|
2631
|
+
`[data-slot="combobox-content"]`
|
|
2632
|
+
|
|
2633
|
+
- `data-closed`: Present while the list is closed, including while it animates out.
|
|
2634
|
+
- `data-presentation` (`popover`, `sheet`): How the list presents this time it's open.
|
|
2635
|
+
- `data-open`: Present while the list is open.
|
|
2636
|
+
- `data-starting-style`: Present for the first frame. Style the state you animate from.
|
|
2637
|
+
- `data-ending-style`: Present while the list animates out. Style the state you animate to.
|
|
2638
|
+
- `data-modality` (`keyboard`, `pointer`): Whether the keyboard or a pointer moves the highlight. Style a focus ring for the keyboard only.
|
|
2639
|
+
- `data-has-icons`: Present while any item inside holds an icon part, so items without one can line up.
|
|
2640
|
+
|
|
2641
|
+
### `<ui-combobox-item>`
|
|
2642
|
+
`[data-slot="combobox-item"]`
|
|
2643
|
+
|
|
2644
|
+
- `data-selected`: Present on selected items, and on the check inside them.
|
|
2645
|
+
- `data-disabled`: Present when the item can't be chosen, or `maxSelected` is reached.
|
|
2646
|
+
- `data-highlighted`: Present on the highlighted item. Focus stays in the input.
|
|
2647
|
+
- `data-has-icon`: Present on an item while it holds an icon part.
|
|
2648
|
+
|
|
2649
|
+
### `<ui-combobox-item-text>`
|
|
2650
|
+
`[data-slot="combobox-item-text"]`
|
|
2651
|
+
|
|
2652
|
+
- `data-matched`: Present on an item's text while the input's text matched part of it, which is marked.
|
|
2653
|
+
|
|
2654
|
+
### `<ui-combobox-item-indicator>`
|
|
2655
|
+
`[data-slot="combobox-item-indicator"]`
|
|
2656
|
+
|
|
2657
|
+
- `data-selected`: Present on selected items, and on the check inside them.
|
|
2658
|
+
|
|
2659
|
+
### `<ui-combobox-empty>`
|
|
2660
|
+
`[data-slot="combobox-empty"]`
|
|
2661
|
+
|
|
2662
|
+
- `data-loading`: Present while results load, with `status="loading"`.
|
|
2663
|
+
- `data-empty`: Present while no item matches.
|
|
2664
|
+
|
|
2665
|
+
### `<ui-combobox-status>`
|
|
2666
|
+
`[data-slot="combobox-status"]`
|
|
2667
|
+
|
|
2668
|
+
- `data-loading`: Present while results load, with `status="loading"`.
|
|
2669
|
+
|
|
2670
|
+
### `<ui-combobox-arrow>`
|
|
2671
|
+
`[data-slot="combobox-arrow"]`
|
|
2672
|
+
|
|
2673
|
+
- `data-closed`: Present while the list is closed, including while it animates out.
|
|
2674
|
+
- `data-open`: Present while the list is open.
|
|
2675
|
+
|
|
2676
|
+
### `<ui-combobox-item-icon>`
|
|
2677
|
+
`[data-slot="combobox-item-icon"]`
|
|
2678
|
+
|
|
2679
|
+
- `data-mirror`: Present on an icon that flips in right-to-left text, with `mirror`.
|
|
2680
|
+
|
|
2681
|
+
### `<ui-combobox-chip>`
|
|
2682
|
+
`[data-slot="combobox-chip"]`
|
|
2683
|
+
|
|
2684
|
+
- `data-value` (`a`): The chip's value, as the string it submits as.
|
|
2685
|
+
- `data-disabled`: Present when the combobox is disabled or read-only, so the chip can't be removed.
|
|
2686
|
+
|
|
2687
|
+
### `<ui-combobox-chip-remove>`
|
|
2688
|
+
`[data-slot="combobox-chip-remove"]`
|
|
2689
|
+
|
|
2690
|
+
- `data-disabled`: Present when the combobox is disabled or read-only.
|
|
2691
|
+
|
|
2692
|
+
### `<ui-combobox-select-all>`
|
|
2693
|
+
`[data-slot="combobox-select-all"]`
|
|
2694
|
+
|
|
2695
|
+
- `data-disabled`: Present when nothing the select-all option covers can be chosen, or choosing them all would pass `maxSelected`.
|
|
2696
|
+
- `data-unchecked`: Present on a select-all option while none of its items is selected.
|
|
2697
|
+
- `data-highlighted`: Present on the highlighted item. Focus stays in the input.
|
|
2698
|
+
|
|
2699
|
+
### `<ui-combobox-create>`
|
|
2700
|
+
`[data-slot="combobox-create"]`
|
|
2701
|
+
|
|
2702
|
+
- `data-highlighted`: Present while the create option is highlighted.
|
|
2703
|
+
- `data-pending`: Present while the Promise `onCreate` returned is running.
|
|
2704
|
+
|
|
2705
|
+
## API Reference
|
|
2706
|
+
|
|
2707
|
+
Props are attributes in kebab case, like `default-value`, `max-selected` and `auto-highlight`: `"false"` is false, and numbers are numbers. `filter` takes `contains`, `startsWith` or `false`. `items`, a `filter` function, `itemToValue`, `itemToLabel` and `isItemEqualToValue` are properties. `container` and `anchor` take CSS selectors. Callbacks are DOM events.
|
|
2708
|
+
|
|
2709
|
+
### Combobox
|
|
2710
|
+
|
|
2711
|
+
- `container?` (`DocumentFragment | Element | null`): Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
|
|
2712
|
+
- `query?` (`string`): The input's text, which filters the list. Pass it to control it. Choosing an item sets it to the item's label, or empties it with `multiple`.
|
|
2713
|
+
- `defaultQuery?` (`string`): The input's text at first, when uncontrolled. Defaults to the selected item's label.
|
|
2714
|
+
- `querychange event`: Runs when the input's text wants to change: typing, choosing an item, or closing with text that wasn't chosen.
|
|
2715
|
+
- `open?` (`boolean`): Whether the list is open. Pass it to control it.
|
|
2716
|
+
- `defaultOpen?` (`boolean`): Whether the list starts open, when uncontrolled.
|
|
2717
|
+
- `openchange event`: Runs when the list wants to open or close. `details.reason` says why, and `details.cancel()` stops it.
|
|
2718
|
+
- `openchangecomplete event`: Runs once the list has finished opening or closing, after any animation.
|
|
2719
|
+
- `maxSelected?` (`number`): With `multiple`, how many items can be selected. Once reached, the other items are disabled.
|
|
2720
|
+
- `items?` (`ListData<T>`): The items as data. Renders the list for you, and filters it without rendering every item.
|
|
2721
|
+
- `emptyItems?` (`ListData<T>`): Items shown instead of `items` while the input is empty, like recent choices or suggestions. Groups work, like `{ type: "group", label: "Recent", items }`. Needs `items`.
|
|
2722
|
+
- `filter?` (`ListFilter<T>`): How typing narrows the list: `"contains"`, `"startsWith"`, a function of the item and the text, whose numeric result also sorts data items, or `false` to keep every item, for results a server already filtered.
|
|
2723
|
+
- `itemToValue?` (`((value: T) => string)`): The string an item's value submits as and is identified by, for object values. Strings pass through, and objects use their `value` or `id`.
|
|
2724
|
+
- `itemToLabel?` (`((value: T) => string)`): The text a value shows, for values whose item isn't rendered, like async results.
|
|
2725
|
+
- `isItemEqualToValue?` (`((itemValue: T, value: T) => boolean)`): Whether an item's value is the selected value, for object values that aren't the same object.
|
|
2726
|
+
- `autoHighlight?` (`"always" | boolean`): Whether the first matching item is highlighted while typing, so Enter chooses it. `"always"` also highlights the selected item, or the first, when the list opens, so arrows move from there.
|
|
2727
|
+
- `keepQuery?` (`boolean`): With `multiple`, whether the input keeps its text after an item is chosen.
|
|
2728
|
+
- `pendingAnnouncement?` (`string`): What screen readers hear once the Create part's `create` has been pending for a second, so a slow action doesn't pass in silence. Set it in the page's language. An empty string says nothing.
|
|
2729
|
+
- `allowCustomValue?` (`boolean`): Whether text that matches no item becomes the value, when Enter is pressed with nothing highlighted, or, without `multiple`, when the list closes. The value is the text itself.
|
|
2730
|
+
- `status?` (`AsyncOptionsStatus`): Whether results are loading or failed, for the status and empty parts. Sets `aria-busy` on the list while loading.
|
|
2731
|
+
- `highlight event`: Runs when the highlight moves, with the item's index among the visible items, so a virtualized list can scroll to it.
|
|
2732
|
+
- `loadmore event`: Runs when the highlight or scrolling reaches the last item, so the next page can load, like `createAsyncOptions`'s `loadMore`. Not again for the same items, unless the last load failed.
|
|
2733
|
+
- `announcement?` (`((change: ListAnnouncement) => string | null | undefined)`): What screen readers hear after typing pauses, when no `ComboboxStatus` is rendered, and when a chip is removed. Return null for nothing.
|
|
2734
|
+
- `loop?` (`boolean`): Whether arrow keys wrap from the last item to the first.
|
|
2735
|
+
- `modal?` (`"pointer" | boolean`): `false`: the page stays usable while the list is open. `"pointer"`: an outside press only closes the list. `true`: also locks scroll and makes the rest of the page inert.
|
|
2736
|
+
- `presentation?` (`PresentationSetting<"popover">`): How the list presents. `"sheet"` slides it up from the bottom of the screen with the input in its header, where it can be swiped away, and `"auto"` does that on small touch screens. A tap that opens a sheet leaves the keyboard closed until the input in the header is tapped. Decided each time it opens; the server renders `"popover"` for `"auto"`.
|
|
2737
|
+
- `disabled?` (`boolean`): Disables the combobox like a native input: it can't be focused or changed, and the value isn't submitted.
|
|
2738
|
+
- `readOnly?` (`boolean`): Keeps the combobox focusable and its value submitted, but stops changes.
|
|
2739
|
+
- `required?` (`boolean`): Blocks form submission until a value is selected, with the browser's own message.
|
|
2740
|
+
- `name?` (`string`): The name the value submits under. Without one, the combobox submits nothing.
|
|
2741
|
+
- `form?` (`string`): The id of a form to belong to, when the combobox isn't inside that form.
|
|
2742
|
+
- `controller?` (`object`): Opens, closes, reads and sets the combobox from anywhere. Create one with `createComboboxController()`.
|
|
2743
|
+
- `dismissible?` (`boolean`): Whether Escape, an outside press, focus or the window leaving, Android's back gesture and a sheet's swipe can close it. Triggers, Close parts and the controller always can. Pass `false` while it holds unsaved work, and ask in `dismissattempt`.
|
|
2744
|
+
- `dismissattempt event`: Runs when `dismissible` refused a dismissal made on purpose: Escape, an outside press, back or a swipe. Focus or the window leaving is refused without it. Ask "Discard changes?" here.
|
|
2745
|
+
- `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
|
|
2746
|
+
- `multiple?` (`boolean`): Whether several items can be selected. The value is then a list, the list stays open as items toggle, and the input empties after each choice. Whether several items can be selected.
|
|
2747
|
+
- `value?` (`T | T[] | null`): The selected value. Pass it to control the combobox. `null` selects nothing. The selected values. Pass it to control the combobox. The selected value, or values with `multiple`. Pass it to control the combobox.
|
|
2748
|
+
- `defaultValue?` (`T | T[] | null`): The value selected at first, when uncontrolled. A form reset returns to it. The values selected at first, when uncontrolled. A form reset returns to them. The value or values selected at first, when uncontrolled. A form reset returns to it.
|
|
2749
|
+
- `valuechange event`: Runs when a user, a form reset or a controller wants to change the value. `details.reason` says why, and `details.cancel()` stops it.
|
|
2750
|
+
|
|
2751
|
+
`announcement` receives `{ type: "results", count, query, loading }` or `{ type: "removed", label }` and returns the words, or `null` for silence. The default is English: "3 results", "No results", "Loading" or "France removed".
|
|
2752
|
+
|
|
2753
|
+
### ComboboxLabel
|
|
2754
|
+
|
|
2755
|
+
The input's visible label. `<ui-combobox-label>` labels the input.
|
|
2756
|
+
|
|
2757
|
+
### ComboboxInput
|
|
2758
|
+
|
|
2759
|
+
The text field that filters the list and shows the chosen label. `<ui-combobox-input>` wraps a native `input`.
|
|
2760
|
+
|
|
2761
|
+
### ComboboxTrigger
|
|
2762
|
+
|
|
2763
|
+
Opens and closes the list. `<ui-combobox-trigger>` wraps a native `button`, kept out of the tab order while the input sits beside it.
|
|
2764
|
+
|
|
2765
|
+
### ComboboxClear
|
|
2766
|
+
|
|
2767
|
+
- `keepMounted?` (`boolean`): Whether the clear button stays in the page while there's nothing to clear.
|
|
2768
|
+
|
|
2769
|
+
### ComboboxChips
|
|
2770
|
+
|
|
2771
|
+
Holds the chips and the input for a multiple selection. `<ui-combobox-chips>` renders a chip for each value.
|
|
2772
|
+
|
|
2773
|
+
### ComboboxValue
|
|
2774
|
+
|
|
2775
|
+
There's no `ui-combobox-value` element: `<ui-combobox-chips>` renders the chips, and the `value` property and `valuechange` event give you the selection for a summary.
|
|
2776
|
+
|
|
2777
|
+
### ComboboxChip
|
|
2778
|
+
|
|
2779
|
+
- `value` (`T`): The selected value this chip shows.
|
|
2780
|
+
|
|
2781
|
+
### ComboboxChipRemove
|
|
2782
|
+
|
|
2783
|
+
Removes its chip's value. `<ui-combobox-chip-remove>` wraps a native `button`. Takes the same props as `ComboboxChip`.
|
|
2784
|
+
|
|
2785
|
+
### ComboboxChipsOverflow
|
|
2786
|
+
|
|
2787
|
+
Turns collapsing on for the chips around it, and shows how many are hidden. `<ui-combobox-chips-overflow>` holds the count.
|
|
2788
|
+
|
|
2789
|
+
### ComboboxValueIcon
|
|
2790
|
+
|
|
2791
|
+
Shows the selected item's icon beside the input. It takes no props of its own, and no children.
|
|
2792
|
+
|
|
2793
|
+
### ComboboxContent
|
|
2794
|
+
|
|
2795
|
+
- `side?` (`Side`): Which side of the anchor the popup opens on. Flips to the other side when there isn't room. Which side of the input the list opens on.
|
|
2796
|
+
- `align?` (`Align`): How the popup lines up with the anchor along that side. `start` and `end` follow the reading direction. How the list lines up with the input along that side.
|
|
2797
|
+
- `sideOffset?` (`number`): Gap between the anchor and the popup, in pixels. Gap between the input and the list, in pixels.
|
|
2798
|
+
- `alignOffset?` (`number`): Shift along the anchor, in pixels. Shift along the input, in pixels.
|
|
2799
|
+
- `collisionPadding?` (`number`): Space to keep from the viewport edges, in pixels.
|
|
2800
|
+
- `arrowPadding?` (`number`): Space to keep between the arrow and the popup's corners, in pixels.
|
|
2801
|
+
- `anchor?` (`string | Anchor | null`): Positions the popup against this instead of its trigger: an element, a CSS selector, a point like `{ x, y }`, or anything with `getBoundingClientRect()`. It's read on every check, so it can move. A controller's `open({ anchor })` wins for that open.
|
|
2802
|
+
- `container?` (`DocumentFragment | Element | null`): Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
|
|
2803
|
+
- `keepMounted?` (`boolean`): Whether the list stays in the page, hidden, while closed.
|
|
2804
|
+
|
|
2805
|
+
### ComboboxArrow
|
|
2806
|
+
|
|
2807
|
+
Points from the popup to its anchor, following the side chosen after collision handling. `<ui-combobox-arrow>` is positioned on the content's edge.
|
|
2808
|
+
|
|
2809
|
+
### ComboboxStatus
|
|
2810
|
+
|
|
2811
|
+
Announces loading, errors and result counts. `<ui-combobox-status>` has the status role.
|
|
2812
|
+
|
|
2813
|
+
### ComboboxEmpty
|
|
2814
|
+
|
|
2815
|
+
Shows while no option matches. `<ui-combobox-empty>` stays hidden while there are matches.
|
|
2816
|
+
|
|
2817
|
+
### ComboboxList
|
|
2818
|
+
|
|
2819
|
+
Holds the options. `<ui-combobox-list>` has the listbox role.
|
|
2820
|
+
|
|
2821
|
+
### ComboboxSelectAll
|
|
2822
|
+
|
|
2823
|
+
Toggles every matching, enabled option in its group, or outside a group. `<ui-combobox-select-all>` has the option role.
|
|
2824
|
+
|
|
2825
|
+
### ComboboxGroup
|
|
2826
|
+
|
|
2827
|
+
Groups options under a label. `<ui-combobox-group>` has the group role.
|
|
2828
|
+
|
|
2829
|
+
### ComboboxGroupLabel
|
|
2830
|
+
|
|
2831
|
+
Names its group. `<ui-combobox-group-label>` labels the group.
|
|
2832
|
+
|
|
2833
|
+
### ComboboxItem
|
|
2834
|
+
|
|
2835
|
+
- `label?` (`string`): The text the input shows once chosen, and filtering matches, when the content isn't plain text. Not the accessible name: use `aria-label` for that.
|
|
2836
|
+
- `keywords?` (`string[]`): More words filtering matches.
|
|
2837
|
+
- `disabled?` (`boolean`): Keeps the item from being chosen. The keyboard and the pointer skip it. Sets `data-disabled` and `aria-disabled`.
|
|
2838
|
+
- `index?` (`number`): The item's position among all visible items, for virtualized lists that render only some. Sets `aria-posinset` and `aria-setsize`.
|
|
2839
|
+
- `value` (`T`): What the value becomes when the item is chosen. A string, or any value with `itemToValue`.
|
|
2840
|
+
|
|
2841
|
+
### ComboboxItemIcon
|
|
2842
|
+
|
|
2843
|
+
- `mirror?` (`boolean`): Flips the icon horizontally in right-to-left text, for icons that point a way, like arrows and chevrons. Icons that picture a thing, like a clock or a check, stay as they are.
|
|
2844
|
+
|
|
2845
|
+
### ComboboxItemText
|
|
2846
|
+
|
|
2847
|
+
Shows its item's label, and marks what matched the query in `<mark>`. `<ui-combobox-item-text>` holds text only.
|
|
2848
|
+
|
|
2849
|
+
### ComboboxItemIndicator
|
|
2850
|
+
|
|
2851
|
+
- `keepMounted?` (`boolean`): Whether the indicator stays in the page while its item isn't selected.
|
|
2852
|
+
|
|
2853
|
+
### ComboboxSeparator
|
|
2854
|
+
|
|
2855
|
+
Separates options or groups. `<ui-combobox-separator>` has the separator role.
|
|
2856
|
+
|
|
2857
|
+
### ComboboxCreate
|
|
2858
|
+
|
|
2859
|
+
- `create event`: Runs with the input's text when the create option is chosen. It doesn't change the value by itself. Return a Promise to mark the option pending until it settles; the list closes (or the input empties, with `multiple`) once it resolves.
|
|
2860
|
+
- `minLength?` (`number`): How many characters the text needs before the option shows.
|
|
2861
|
+
|
|
2862
|
+
### Items as Data
|
|
2863
|
+
|
|
2864
|
+
Each item in `items`. Groups are `{ type: "group", label, items }` and separators `{ type: "separator" }`.
|
|
2865
|
+
|
|
2866
|
+
- `value` (`T`): Identifies the item, and is what choosing it reports. A string, or any value with `itemToValue`.
|
|
2867
|
+
- `label` (`string`): The text shown and matched by typeahead and filtering. A select's trigger shows it once chosen.
|
|
2868
|
+
- `keywords?` (`string[]`): More words filtering matches, like `["team"]` for a plan called Pro.
|
|
2869
|
+
- `disabled?` (`boolean`): Keeps the item from being chosen. The keyboard skips it, like in a native select.
|
|
2870
|
+
|
|
2871
|
+
The whole array is a `ListData`, or `ListData<Item>` when values are objects.
|
|
2872
|
+
|
|
2873
|
+
### Async Options
|
|
2874
|
+
|
|
2875
|
+
[Async Options](../utilities/async-options.md) covers loading, paging, and retries.
|
|
2876
|
+
|
|
2877
|
+
### State
|
|
2878
|
+
|
|
2879
|
+
The same state shows as data attributes on each element.
|
|
2880
|
+
|
|
2881
|
+
`ComboboxInput`:
|
|
2882
|
+
|
|
2883
|
+
- `open` (`boolean`): Whether the list is open.
|
|
2884
|
+
- `disabled` (`boolean`): Whether the combobox is disabled, by itself or by a `<fieldset>`.
|
|
2885
|
+
- `readOnly` (`boolean`): Whether the combobox keeps its value but refuses changes, from its `readOnly` option.
|
|
2886
|
+
- `required` (`boolean`): Whether a value must be selected before the form submits, from its `required` option.
|
|
2887
|
+
|
|
2888
|
+
`ComboboxTrigger`:
|
|
2889
|
+
|
|
2890
|
+
- `open` (`boolean`): Whether the list is open.
|
|
2891
|
+
- `disabled` (`boolean`): Whether the combobox is disabled, by itself or by a `<fieldset>`.
|
|
2892
|
+
- `readOnly` (`boolean`): Whether the combobox keeps its value but refuses changes, from its `readOnly` option.
|
|
2893
|
+
- `required` (`boolean`): Whether a value must be selected before the form submits, from its `required` option.
|
|
2894
|
+
|
|
2895
|
+
`ComboboxContent` and `ComboboxList`:
|
|
2896
|
+
|
|
2897
|
+
- `open` (`boolean`): Whether the list is open. False while it animates out.
|
|
2898
|
+
- `presentation` (`"popover" | "sheet"`): How it presents this time it's open.
|
|
2899
|
+
- `multiple` (`boolean | undefined`): Whether several items can be selected.
|
|
2900
|
+
- `status` (`AsyncOptionsStatus`): Whether results are loading or failed.
|
|
2901
|
+
- `items` (`ListData<unknown>`): The visible items, for rendering a virtualized list yourself.
|
|
2902
|
+
|
|
2903
|
+
`ComboboxItem`:
|
|
2904
|
+
|
|
2905
|
+
- `highlighted` (`boolean`): Whether the item is highlighted, by the keyboard or the pointer.
|
|
2906
|
+
- `selected` (`boolean`): Whether its value is in the combobox's value, compared with `isItemEqualToValue` when you pass one.
|
|
2907
|
+
- `disabled` (`boolean`): Whether the item can't be chosen, by itself or because `maxSelected` is reached.
|
|
2908
|
+
- `maxReached` (`boolean`): Whether `maxSelected` is reached and the item isn't selected, which is why it's disabled.
|
|
2909
|
+
|
|
2910
|
+
`ComboboxItemIndicator`:
|
|
2911
|
+
|
|
2912
|
+
- `selected` (`boolean`): Whether its item is selected. While false, the indicator isn't shown unless `keepMounted` is set.
|
|
2913
|
+
|
|
2914
|
+
`ComboboxItemIcon`:
|
|
2915
|
+
|
|
2916
|
+
- `mirror` (`boolean`): Whether it flips in right-to-left text.
|
|
2917
|
+
|
|
2918
|
+
`ComboboxCreate`:
|
|
2919
|
+
|
|
2920
|
+
- `query` (`string`): The text the option would create.
|
|
2921
|
+
- `highlighted` (`boolean`): Whether the option is highlighted.
|
|
2922
|
+
- `pending` (`boolean`): Whether `onCreate`'s Promise is running.
|
|
2923
|
+
- `error` (`unknown`): Why the last `onCreate` failed, until the next try.
|
|
2924
|
+
|
|
2925
|
+
`ComboboxEmpty`:
|
|
2926
|
+
|
|
2927
|
+
- `loading` (`boolean`): Whether results are loading.
|
|
2928
|
+
- `empty` (`boolean`): Whether no item matches.
|
|
2929
|
+
- `query` (`string`): The text the list is filtered by.
|
|
2930
|
+
|
|
2931
|
+
`ComboboxStatus`:
|
|
2932
|
+
|
|
2933
|
+
- `status` (`"idle" | "loading" | "error"`): Whether results are loading or failed.
|
|
2934
|
+
- `loading` (`boolean`): Whether results are loading.
|
|
2935
|
+
- `count` (`number`): How many items are visible.
|
|
2936
|
+
|
|
2937
|
+
`ComboboxValue`:
|
|
2938
|
+
|
|
2939
|
+
- `value` (`unknown`): The selected value: one, `null`, or a list with `multiple`.
|
|
2940
|
+
- `labels` (`string[]`): Each selected item's label, in the order they were selected.
|
|
2941
|
+
- `label` (`string`): The labels joined with commas, or with `maxLabels`, as a list in the page's language, like "France, Chile and 2 more". An empty string for none.
|
|
2942
|
+
|
|
2943
|
+
`ComboboxChip`:
|
|
2944
|
+
|
|
2945
|
+
- `value` (`unknown`): The selected value the chip stands for, from its `value` prop.
|
|
2946
|
+
- `label` (`string`): The text for the value: from `itemToLabel`, its item's label, or the value itself.
|
|
2947
|
+
- `disabled` (`boolean`): Whether the combobox is disabled or read-only, so the chip can't be removed.
|
|
2948
|
+
|
|
2949
|
+
`ComboboxClear` and `ComboboxChipRemove`:
|
|
2950
|
+
|
|
2951
|
+
- `disabled` (`boolean`): Whether the combobox is disabled or read-only.
|
|
2952
|
+
|
|
2953
|
+
`ComboboxArrow`:
|
|
2954
|
+
|
|
2955
|
+
- `open` (`boolean`): Whether the list is open.
|
|
2956
|
+
|
|
2957
|
+
`ComboboxItemText`:
|
|
2958
|
+
|
|
2959
|
+
- `segments` (`MatchSegment[]`): The text in pieces, each marked when it matched. Joined, they're the whole text.
|
|
2960
|
+
- `matched` (`boolean`): Whether any piece matched.
|
|
2961
|
+
|
|
2962
|
+
`ComboboxSelectAll`:
|
|
2963
|
+
|
|
2964
|
+
- `checked` (`boolean`): Whether every item it covers is selected.
|
|
2965
|
+
- `indeterminate` (`boolean`): Whether some of its items are selected, but not all.
|
|
2966
|
+
- `highlighted` (`boolean`): Whether it's highlighted, by the keyboard or the pointer.
|
|
2967
|
+
- `disabled` (`boolean`): Whether it can't be used: nothing it covers can be chosen, or choosing them all would pass `maxSelected`.
|
|
2968
|
+
|
|
2969
|
+
`ComboboxChipsOverflow`:
|
|
2970
|
+
|
|
2971
|
+
- `count` (`number`): How many chips don't fit and are hidden.
|
|
2972
|
+
- `labels` (`string[]`): The labels of the hidden chips.
|
|
2973
|
+
|
|
2974
|
+
### Reasons
|
|
2975
|
+
|
|
2976
|
+
`event.detail.reason` says why the value changed:
|
|
2977
|
+
|
|
2978
|
+
- `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
|
|
2979
|
+
- `input-change` (`Event`): Typing, pasting or dropping changed a text field's value; in a combobox it can also open or close it, or Enter keeps the typed text as the value.
|
|
2980
|
+
- `clear-press` (`MouseEvent | KeyboardEvent`): A Clear part was pressed, like on an input, a select or a combobox.
|
|
2981
|
+
- `remove-press` (`MouseEvent | KeyboardEvent`): A chip's remove part was pressed, or Backspace or Delete removed a chip or a span.
|
|
2982
|
+
- `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
|
|
2983
|
+
- `reset` (`none`): A form reset.
|
|
2984
|
+
- `restore` (`none`): The browser restored state, like after going back, or a saved value was read back from storage.
|
|
2985
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
2986
|
+
|
|
2987
|
+
Why the text changed:
|
|
2988
|
+
|
|
2989
|
+
- `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
|
|
2990
|
+
- `input-change` (`Event`): Typing, pasting or dropping changed a text field's value; in a combobox it can also open or close it, or Enter keeps the typed text as the value.
|
|
2991
|
+
- `clear-press` (`MouseEvent | KeyboardEvent`): A Clear part was pressed, like on an input, a select or a combobox.
|
|
2992
|
+
- `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
|
|
2993
|
+
- `reset` (`none`): A form reset.
|
|
2994
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
2995
|
+
- `trigger-press` (`MouseEvent | KeyboardEvent | TouchEvent`): Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its `touchend`.
|
|
2996
|
+
- `close-press` (`MouseEvent | KeyboardEvent | SubmitEvent`): A Close part was pressed, or a form with `method="dialog"` was submitted.
|
|
2997
|
+
- `outside-press` (`MouseEvent`): A press outside the layer.
|
|
2998
|
+
- `focus-out` (`KeyboardEvent | FocusEvent`): Focus left the layer, or Tab moved it out.
|
|
2999
|
+
- `window-blur` (`FocusEvent`): The window lost focus.
|
|
3000
|
+
- `back` (`Event`): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
|
|
3001
|
+
- `swipe` (`TouchEvent | PointerEvent`): A swipe gesture: a finger, mouse or pen dragged it.
|
|
3002
|
+
|
|
3003
|
+
And why the list opened or closed:
|
|
3004
|
+
|
|
3005
|
+
- `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
|
|
3006
|
+
- `input-change` (`Event`): Typing, pasting or dropping changed a text field's value; in a combobox it can also open or close it, or Enter keeps the typed text as the value.
|
|
3007
|
+
- `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
|
|
3008
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
3009
|
+
- `trigger-press` (`MouseEvent | KeyboardEvent | TouchEvent`): Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its `touchend`.
|
|
3010
|
+
- `close-press` (`MouseEvent | KeyboardEvent | SubmitEvent`): A Close part was pressed, or a form with `method="dialog"` was submitted.
|
|
3011
|
+
- `outside-press` (`MouseEvent`): A press outside the layer.
|
|
3012
|
+
- `focus-out` (`KeyboardEvent | FocusEvent`): Focus left the layer, or Tab moved it out.
|
|
3013
|
+
- `window-blur` (`FocusEvent`): The window lost focus.
|
|
3014
|
+
- `back` (`Event`): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
|
|
3015
|
+
- `swipe` (`TouchEvent | PointerEvent`): A swipe gesture: a finger, mouse or pen dragged it.
|
|
3016
|
+
|
|
3017
|
+
With `dismissible` false, the refused dismissals `dismissattempt` reports:
|
|
3018
|
+
|
|
3019
|
+
- `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
|
|
3020
|
+
- `outside-press` (`MouseEvent`): A press outside the layer.
|
|
3021
|
+
- `back` (`Event`): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
|
|
3022
|
+
- `swipe` (`TouchEvent | PointerEvent`): A swipe gesture: a finger, mouse or pen dragged it.
|
|
3023
|
+
|
|
3024
|
+
### Controller
|
|
3025
|
+
|
|
3026
|
+
The element's `api` property has the same methods.
|
|
3027
|
+
|
|
3028
|
+
Every write method takes an optional `{ reason, event }`, which the change event carries.
|
|
3029
|
+
|
|
3030
|
+
- `isOpen()` (`() => boolean`): Whether the list is open.
|
|
3031
|
+
- `setOpen(open, options?)` (`(open: boolean, options?: ComboboxChangeOptions<ComboboxOpenChangeReason> | undefined) => boolean`): Opens or closes the list. Returns false when the change was canceled or it was already open or closed.
|
|
3032
|
+
- `open(options?)` (`(options?: (ComboboxChangeOptions<ComboboxOpenChangeReason> & { signal?: AbortSignal | undefined; }) | undefined) => Promise<...>`): Opens the list, and resolves once it closes, with why and how. Resolves exactly once and never rejects: on the next close from any source, when `signal` aborts, or with reason `"unmount"` when the combobox leaves the page.
|
|
3033
|
+
- `close(options?)` (`(options?: ComboboxChangeOptions<ComboboxOpenChangeReason> | undefined) => boolean`): Closes the list. Returns false when the close was canceled or it was already closed.
|
|
3034
|
+
- `toggle(options?)` (`(options?: ComboboxChangeOptions<ComboboxOpenChangeReason> | undefined) => boolean`): Opens the list when it's closed, and closes it when it's open. Returns false when the change was canceled.
|
|
3035
|
+
- `getValue()` (`() => ComboboxValue<T, Multiple>`): The selected value.
|
|
3036
|
+
- `setValue(value, options?)` (`(value: ComboboxValue<T, Multiple>, options?: ComboboxChangeOptions<ComboboxValueChangeReason> | undefined) => boolean`): Selects a value, as a user would. A disabled or read-only combobox ignores it. Returns false when the change was canceled or nothing changed.
|
|
3037
|
+
- `clear(options?)` (`(options?: ComboboxChangeOptions<ComboboxValueChangeReason> | undefined) => boolean`): Selects nothing and empties the input. Returns false when the change was canceled or nothing changed.
|
|
3038
|
+
- `getQuery()` (`() => string`): The input's text.
|
|
3039
|
+
- `setQuery(query, options?)` (`(query: string, options?: ComboboxChangeOptions<ComboboxQueryChangeReason> | undefined) => boolean`): Changes the input's text, which filters the list. Returns false when the change was canceled or nothing changed.
|
|
3040
|
+
- `focus()` (`() => void`): Moves focus to the input.
|
|
3041
|
+
|
|
3042
|
+
### Events
|
|
3043
|
+
|
|
3044
|
+
Events fire on their own element and don't bubble, with the new state in `event.detail.value` and the `reason` and `event` beside it. Calling `preventDefault()` on a change event cancels it.
|
|
3045
|
+
|
|
3046
|
+
| Member | On | Description |
|
|
3047
|
+
| -------------------------- | ------------- | -------------------------------------------------------------------- |
|
|
3048
|
+
| `value` property | `ui-combobox` | The selected value. Setting it selects, as a controller would |
|
|
3049
|
+
| `query` property | `ui-combobox` | The input's text |
|
|
3050
|
+
| `open` property, attribute | `ui-combobox` | Whether the list is open. The attribute sets where it starts |
|
|
3051
|
+
| `api` property | `ui-combobox` | The [controller](#controller-1) |
|
|
3052
|
+
| `valuechange` event | `ui-combobox` | `detail: { value, reason, event, source }` |
|
|
3053
|
+
| `querychange` event | `ui-combobox` | `detail: { value: query, reason, event, source }` |
|
|
3054
|
+
| `openchange` event | `ui-combobox` | `detail: { value: open, reason, event, source }` |
|
|
3055
|
+
| `openchangecomplete` event | `ui-combobox` | `detail: { value: open }`, after the animation |
|
|
3056
|
+
| `highlight` event | `ui-combobox` | `detail: { value, index, event }` |
|
|
3057
|
+
| `loadmore` event | `ui-combobox` | When the last item is reached |
|
|
3058
|
+
| `create` event | `ui-combobox` | `detail: { value: query, waitUntil(promise) }`, from the create part |
|
|
3059
|
+
| `dismissattempt` event | `ui-combobox` | `detail: { reason, event }`, when a dismissal was refused |
|
|
3060
|
+
|
|
3061
|
+
## Accessibility
|
|
3062
|
+
|
|
3063
|
+
The combobox follows the [WAI-ARIA combobox pattern with list autocomplete](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/). Focus usually stays in the input while `aria-activedescendant` identifies the highlighted option. A touch screen-reader trigger can move real focus into the list, and a sheet has its own [focus behavior](#presentation). Every line below is an automated test.
|
|
3064
|
+
|
|
3065
|
+
### Keyboard
|
|
3066
|
+
|
|
3067
|
+
| Key | Does |
|
|
3068
|
+
| --------------------- | --------------------------------------------------------------------- |
|
|
3069
|
+
| Typing | Filters the options and opens the list |
|
|
3070
|
+
| ArrowDown, ArrowUp | Opens the list, then moves the highlight. Wraps unless `loop="false"` |
|
|
3071
|
+
| Enter | Chooses the highlighted option |
|
|
3072
|
+
| Escape | Closes the list, then restores the selection's label |
|
|
3073
|
+
| Backspace | In an empty input with chips, removes the last chip |
|
|
3074
|
+
| ArrowLeft, ArrowRight | Moves between chips, reversed in right-to-left text |
|
|
3075
|
+
|
|
3076
|
+
### Tested Behavior
|
|
3077
|
+
|
|
3078
|
+
**Input**
|
|
3079
|
+
|
|
3080
|
+
- Is a combobox named by its label, with list autocomplete.
|
|
3081
|
+
- Typing opens the list with the items that match.
|
|
3082
|
+
- Matching ignores case and accents.
|
|
3083
|
+
- ArrowDown opens the list.
|
|
3084
|
+
- Choosing with the keyboard leaves the caret after the text.
|
|
3085
|
+
- Points to the list it controls while open.
|
|
3086
|
+
|
|
3087
|
+
**Highlight**
|
|
3088
|
+
|
|
3089
|
+
- Arrow keys move the highlight while focus stays in the input.
|
|
3090
|
+
- Arrow keys skip disabled items.
|
|
3091
|
+
- Moving over a disabled item leaves the highlight where it was.
|
|
3092
|
+
- The highlighted option has the id aria-activedescendant names.
|
|
3093
|
+
|
|
3094
|
+
**AutoHighlight**
|
|
3095
|
+
|
|
3096
|
+
- The first match is highlighted from the first keystroke, so Enter chooses it.
|
|
3097
|
+
- A disabled match is passed over.
|
|
3098
|
+
|
|
3099
|
+
**Choosing**
|
|
3100
|
+
|
|
3101
|
+
- Enter chooses the highlighted item, puts its label in the input and closes.
|
|
3102
|
+
- Clicking an item chooses it and keeps focus in the input.
|
|
3103
|
+
- A disabled item can't be chosen.
|
|
3104
|
+
- After choosing, opening again shows every item with the chosen one selected.
|
|
3105
|
+
- A canceled choice closes the list and keeps the value's label in the input.
|
|
3106
|
+
- Text that matches nothing shows the empty message.
|
|
3107
|
+
|
|
3108
|
+
**Closing**
|
|
3109
|
+
|
|
3110
|
+
- Escape closes the list and keeps focus in the input.
|
|
3111
|
+
- Escape after deleting the text brings back the value and its label.
|
|
3112
|
+
- Leaving after deleting the text keeps the value empty.
|
|
3113
|
+
- Clicking outside closes the list.
|
|
3114
|
+
- Tab closes the list and moves on.
|
|
3115
|
+
|
|
3116
|
+
**Trigger and clear**
|
|
3117
|
+
|
|
3118
|
+
- The trigger button opens and closes the list.
|
|
3119
|
+
- Clear empties the input and the value.
|
|
3120
|
+
|
|
3121
|
+
**Default value**
|
|
3122
|
+
|
|
3123
|
+
- Shows its label in the input.
|
|
3124
|
+
|
|
3125
|
+
**Multiple**
|
|
3126
|
+
|
|
3127
|
+
- Choosing adds a chip and empties the input.
|
|
3128
|
+
- A canceled choice adds no chip and keeps the text.
|
|
3129
|
+
- Backspace in an empty input removes the last chip.
|
|
3130
|
+
- A chip's remove button removes its value.
|
|
3131
|
+
|
|
3132
|
+
**Groups**
|
|
3133
|
+
|
|
3134
|
+
- Items sit in groups named by their labels, and empty groups hide while filtering.
|
|
3135
|
+
|
|
3136
|
+
**Forms**
|
|
3137
|
+
|
|
3138
|
+
- The form submits the value under the combobox's name.
|
|
3139
|
+
- Several values submit under one name.
|
|
3140
|
+
- A required combobox blocks submission until a value is chosen.
|
|
3141
|
+
- A disabled combobox can't be focused.
|
|
3142
|
+
- A disabled fieldset disables the combobox and leaves it out of the form.
|
|
3143
|
+
|
|
3144
|
+
**Async**
|
|
3145
|
+
|
|
3146
|
+
- Results load for the text, with the list kept while loading.
|
|
3147
|
+
- The chosen item's label survives results that don't include it.
|
|
3148
|
+
|
|
3149
|
+
Runs in React, Vue and web components.
|
|
3150
|
+
|
|
3151
|
+
Long lists stay fast and announce their size:
|
|
3152
|
+
|
|
3153
|
+
**Virtualized**
|
|
3154
|
+
|
|
3155
|
+
- Renders only the rows in view, each with its place in the whole list.
|
|
3156
|
+
- Arrow keys reach items that weren't rendered, and the active descendant follows.
|
|
3157
|
+
- Filtering covers every item, rendered or not.
|
|
3158
|
+
|
|
3159
|
+
Runs in React.
|
|
3160
|
+
|
|
3161
|
+
Also tested:
|
|
3162
|
+
|
|
3163
|
+
**Enhancements**
|
|
3164
|
+
|
|
3165
|
+
- The list is named by the combobox's label.
|
|
3166
|
+
- Filtering matches an item's keywords as well as its label.
|
|
3167
|
+
- A create option offers the text, and hides when an item's label matches it exactly.
|
|
3168
|
+
- Enter on the create option calls onCreate with the text.
|
|
3169
|
+
- A create option is pending while onCreate's Promise runs.
|
|
3170
|
+
- With multiple, the list stays open after choosing.
|
|
3171
|
+
- MaxSelected disables the other items once reached.
|
|
3172
|
+
- Arrow keys move between chips, and Backspace on one removes it.
|
|
3173
|
+
- The chips are a toolbar named by the combobox's label.
|
|
3174
|
+
- Web chips follow a label that changes after they rendered. Web components only.
|
|
3175
|
+
- Enter on the trigger opens the list as the keyboard did.
|
|
3176
|
+
- ArrowDown on the trigger opens the list at its first item.
|
|
3177
|
+
- The content says whether the keyboard or a pointer is in use, while focus stays in the input.
|
|
3178
|
+
- A form reset returns the combobox to its default value and label.
|
|
3179
|
+
- Arrow keys keep the highlighted item in view when the … scrolls.
|
|
3180
|
+
|
|
3181
|
+
Runs in React, Vue and web components.
|
|
3182
|
+
|
|
3183
|
+
Rows people rebuild, here and in Select:
|
|
3184
|
+
|
|
3185
|
+
**Items shown while nothing is typed**
|
|
3186
|
+
|
|
3187
|
+
- A combobox shows its recent items, as a group, until the user types.
|
|
3188
|
+
- An autocomplete shows recent searches while the field is empty.
|
|
3189
|
+
|
|
3190
|
+
**Select all**
|
|
3191
|
+
|
|
3192
|
+
- Selects every item that can be chosen, then clears them, and says which with aria-checked.
|
|
3193
|
+
- Is mixed while some are selected.
|
|
3194
|
+
- Covers only the items the text matches.
|
|
3195
|
+
- Is reached with the arrow keys and chosen with Enter.
|
|
3196
|
+
- In a group, it covers that group.
|
|
3197
|
+
- A select's works the same, with real focus.
|
|
3198
|
+
|
|
3199
|
+
**Chips that collapse**
|
|
3200
|
+
|
|
3201
|
+
- Chips past the line hide, and a count says how many.
|
|
3202
|
+
- Removing chips brings the hidden ones back, and the count goes.
|
|
3203
|
+
- A value text lists two labels and counts the rest, in the page's language. React and Vue only.
|
|
3204
|
+
- A select's value lists two labels and counts the rest.
|
|
3205
|
+
- A web select value takes getMoreLabel as a property. Web components only.
|
|
3206
|
+
|
|
3207
|
+
**Loading more**
|
|
3208
|
+
|
|
3209
|
+
- Reaching the last item with the keyboard loads the next page.
|
|
3210
|
+
- Scrolling to the end loads the next page, with a status while it does.
|
|
3211
|
+
- The last page loads no more.
|
|
3212
|
+
|
|
3213
|
+
**Rows**
|
|
3214
|
+
|
|
3215
|
+
- A recent item is chosen like any other.
|
|
3216
|
+
- Typing on a select never lands on it.
|
|
3217
|
+
|
|
3218
|
+
Runs in React, Vue and web components.
|
|
3219
|
+
|
|
3220
|
+
Match highlighting, here and in Autocomplete and Command:
|
|
3221
|
+
|
|
3222
|
+
**Combobox**
|
|
3223
|
+
|
|
3224
|
+
- Marks the part of each label the text matched.
|
|
3225
|
+
- Marks keep the label's own case and accents while matching without them.
|
|
3226
|
+
- An item matched by a keyword has nothing to mark.
|
|
3227
|
+
- Marks go away when the text is cleared, and the label reads whole.
|
|
3228
|
+
|
|
3229
|
+
**Autocomplete**
|
|
3230
|
+
|
|
3231
|
+
- Marks the part of each suggestion the text matched.
|
|
3232
|
+
|
|
3233
|
+
**Command**
|
|
3234
|
+
|
|
3235
|
+
- Marks each letter a fuzzy match found, joining neighbours.
|
|
3236
|
+
- Marks a whole match at a word start.
|
|
3237
|
+
|
|
3238
|
+
**Highlighting**
|
|
3239
|
+
|
|
3240
|
+
- The chosen label in the input shows every item unmarked.
|
|
3241
|
+
|
|
3242
|
+
Runs in React, Vue and web components.
|
|
3243
|
+
|
|
3244
|
+
Touch screen readers and announcements:
|
|
3245
|
+
|
|
3246
|
+
**A screen reader's tap moves real focus into the list**
|
|
3247
|
+
|
|
3248
|
+
- Autocomplete: tapping the trigger focuses the first suggestion, and Escape returns.
|
|
3249
|
+
- Searchable select: tapping the trigger focuses the selected item, not the search field.
|
|
3250
|
+
|
|
3251
|
+
**Combobox**
|
|
3252
|
+
|
|
3253
|
+
- Tapping the trigger focuses the selected item, with no aria-activedescendant.
|
|
3254
|
+
- Arrow keys move real focus, and choosing returns focus to the input.
|
|
3255
|
+
- Escape closes the list and focus returns to the input.
|
|
3256
|
+
|
|
3257
|
+
**Announcements**
|
|
3258
|
+
|
|
3259
|
+
- How many items match is announced once typing pauses.
|
|
3260
|
+
- Autocomplete and searchable select announce counts too.
|
|
3261
|
+
- Removing a chip announces which.
|
|
3262
|
+
|
|
3263
|
+
**Touch screen readers and announcements**
|
|
3264
|
+
|
|
3265
|
+
- A tap on the input is for typing: focus stays there.
|
|
3266
|
+
- A status part says it instead, so nothing is announced twice.
|
|
3267
|
+
|
|
3268
|
+
Runs in React, Vue and web components.
|
|
3269
|
+
|
|
3270
|
+
Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:
|
|
3271
|
+
|
|
3272
|
+
- Announcements while filtering and choosing with a keyboard.
|
|
3273
|
+
|
|
3274
|
+
Runs in React, Vue and web components.
|