@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,1401 @@
|
|
|
1
|
+
# Accordion
|
|
2
|
+
|
|
3
|
+
Show and hide sections of content from their headings.
|
|
4
|
+
|
|
5
|
+
Use an Accordion for content people can explore one section at a time, such as answers, settings, or a course outline. Each heading is a button that opens its panel.
|
|
6
|
+
|
|
7
|
+
```ts title="basic.ts"
|
|
8
|
+
import "@ui-sdk/web/accordion"
|
|
9
|
+
import { ChevronDown, createElement } from "lucide"
|
|
10
|
+
|
|
11
|
+
const items = [
|
|
12
|
+
{
|
|
13
|
+
value: "shipping",
|
|
14
|
+
label: "What are your shipping options?",
|
|
15
|
+
content: [
|
|
16
|
+
"We offer standard (5 to 7 days), express (2 to 3 days) and overnight shipping.",
|
|
17
|
+
"Every order ships with tracking, and orders over $50 ship free.",
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
value: "returns",
|
|
22
|
+
label: "What is your return policy?",
|
|
23
|
+
content: [
|
|
24
|
+
"Return any unused item in its original packaging within 30 days. Refunds arrive within 5 to 7 business days.",
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
value: "support",
|
|
29
|
+
label: "How can I reach support?",
|
|
30
|
+
content: [
|
|
31
|
+
"Write to us by email or chat, or call us. We answer within 24 hours on business days.",
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
function mount(root: HTMLElement) {
|
|
37
|
+
root.innerHTML = `
|
|
38
|
+
<ui-accordion default-value="shipping">
|
|
39
|
+
${items
|
|
40
|
+
.map(
|
|
41
|
+
(item) => `
|
|
42
|
+
<ui-accordion-item value="${item.value}">
|
|
43
|
+
<ui-accordion-header>
|
|
44
|
+
<h3>
|
|
45
|
+
<ui-accordion-trigger>
|
|
46
|
+
<button type="button" aria-expanded="${item.value === "shipping"}">
|
|
47
|
+
${item.label}
|
|
48
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
49
|
+
</button>
|
|
50
|
+
</ui-accordion-trigger>
|
|
51
|
+
</h3>
|
|
52
|
+
</ui-accordion-header>
|
|
53
|
+
<ui-accordion-panel${item.value === "shipping" ? "" : " hidden"}>
|
|
54
|
+
${item.content.map((paragraph) => `<p>${paragraph}</p>`).join("")}
|
|
55
|
+
</ui-accordion-panel>
|
|
56
|
+
</ui-accordion-item>`
|
|
57
|
+
)
|
|
58
|
+
.join("")}
|
|
59
|
+
</ui-accordion>
|
|
60
|
+
`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { mount }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Press “What is your return policy?”. Its answer opens and the shipping answer closes. Press the same heading again to close it.
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install @ui-sdk/web @ui-sdk/core
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Import the component to register its elements. The import is safe on the server.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import "@ui-sdk/web/accordion"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Composition
|
|
81
|
+
|
|
82
|
+
Use the following composition to build a `ui-accordion`:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
ui-accordion
|
|
86
|
+
├── ui-accordion-item
|
|
87
|
+
│ ├── ui-accordion-header
|
|
88
|
+
│ │ └── h3
|
|
89
|
+
│ │ └── ui-accordion-trigger
|
|
90
|
+
│ │ └── button
|
|
91
|
+
│ └── ui-accordion-panel
|
|
92
|
+
└── ui-accordion-item
|
|
93
|
+
├── ui-accordion-header
|
|
94
|
+
│ └── h3
|
|
95
|
+
│ └── ui-accordion-trigger
|
|
96
|
+
│ └── button
|
|
97
|
+
└── ui-accordion-panel
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Give every item a unique `value`. Put its trigger inside a heading, then add the panel it opens. The accordion's value is always an array of open item values, even when only one can be open.
|
|
101
|
+
|
|
102
|
+
The header wraps your heading and the trigger wraps your button. Use a heading level that fits the surrounding document. Tab moves between triggers; Enter or Space opens and closes their panels. Arrow keys, Home and End do nothing here: the APG removed them from the pattern, and a screen reader’s heading navigation already reaches each trigger.
|
|
103
|
+
|
|
104
|
+
## Usage
|
|
105
|
+
|
|
106
|
+
Import the parts and put them together:
|
|
107
|
+
|
|
108
|
+
```html
|
|
109
|
+
<script type="module">
|
|
110
|
+
import "@ui-sdk/web/accordion"
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<ui-accordion>
|
|
114
|
+
<ui-accordion-item value="shipping">
|
|
115
|
+
<ui-accordion-header>
|
|
116
|
+
<h3>
|
|
117
|
+
<ui-accordion-trigger><button>Shipping</button></ui-accordion-trigger>
|
|
118
|
+
</h3>
|
|
119
|
+
</ui-accordion-header>
|
|
120
|
+
<ui-accordion-panel hidden
|
|
121
|
+
>Orders arrive in five to seven days.</ui-accordion-panel
|
|
122
|
+
>
|
|
123
|
+
</ui-accordion-item>
|
|
124
|
+
</ui-accordion>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Build Your Own
|
|
128
|
+
|
|
129
|
+
`@ui-sdk/web/accordion` comes with ready-made `ui-accordion` elements. If you'd like to build your own, with your own names, start from the `accordion` primitive in `@ui-sdk/core`:
|
|
130
|
+
|
|
131
|
+
```ts title="faq-elements.ts"
|
|
132
|
+
import { accordion } from "@ui-sdk/core/accordion"
|
|
133
|
+
import { createParts, wrappedButton } from "@ui-sdk/web"
|
|
134
|
+
|
|
135
|
+
createParts(accordion, {
|
|
136
|
+
root: "faq-list",
|
|
137
|
+
item: "faq-item",
|
|
138
|
+
header: {
|
|
139
|
+
tag: "faq-heading",
|
|
140
|
+
target: (el) => el.querySelector<HTMLElement>("h3") ?? el,
|
|
141
|
+
},
|
|
142
|
+
trigger: { tag: "faq-question", target: wrappedButton, display: "contents" },
|
|
143
|
+
panel: "faq-answer",
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<faq-list>
|
|
149
|
+
<faq-item value="shipping">
|
|
150
|
+
<faq-heading>
|
|
151
|
+
<h3>
|
|
152
|
+
<faq-question
|
|
153
|
+
><button>What are your shipping options?</button></faq-question
|
|
154
|
+
>
|
|
155
|
+
</h3>
|
|
156
|
+
</faq-heading>
|
|
157
|
+
<faq-answer>Standard, express and overnight.</faq-answer>
|
|
158
|
+
</faq-item>
|
|
159
|
+
</faq-list>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Each part keeps the behavior, state and data attributes of its ready-made counterpart. See [Rendering](../rendering.md) and [Custom Parts](../custom-parts.md) for more.
|
|
163
|
+
|
|
164
|
+
## Multiple
|
|
165
|
+
|
|
166
|
+
Add `multiple` to let several items stay open. Opening one then leaves the others as they are.
|
|
167
|
+
|
|
168
|
+
```ts title="multiple.ts"
|
|
169
|
+
import "@ui-sdk/web/accordion"
|
|
170
|
+
import { ChevronDown, createElement } from "lucide"
|
|
171
|
+
|
|
172
|
+
const items = [
|
|
173
|
+
{
|
|
174
|
+
value: "notifications",
|
|
175
|
+
label: "Notification settings",
|
|
176
|
+
content: "Choose what we email you about, and how often.",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
value: "privacy",
|
|
180
|
+
label: "Privacy and security",
|
|
181
|
+
content: "Turn on two-factor authentication and review signed-in devices.",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
value: "billing",
|
|
185
|
+
label: "Billing and subscription",
|
|
186
|
+
content: "Update your payment method and download past invoices.",
|
|
187
|
+
},
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
function mount(root: HTMLElement) {
|
|
191
|
+
root.innerHTML = `
|
|
192
|
+
<ui-accordion multiple default-value="notifications">
|
|
193
|
+
${items
|
|
194
|
+
.map(
|
|
195
|
+
(item) => `
|
|
196
|
+
<ui-accordion-item value="${item.value}">
|
|
197
|
+
<ui-accordion-header>
|
|
198
|
+
<h3>
|
|
199
|
+
<ui-accordion-trigger>
|
|
200
|
+
<button type="button" aria-expanded="${item.value === "notifications"}">
|
|
201
|
+
${item.label}
|
|
202
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
203
|
+
</button>
|
|
204
|
+
</ui-accordion-trigger>
|
|
205
|
+
</h3>
|
|
206
|
+
</ui-accordion-header>
|
|
207
|
+
<ui-accordion-panel${item.value === "notifications" ? "" : " hidden"}>
|
|
208
|
+
${item.content}
|
|
209
|
+
</ui-accordion-panel>
|
|
210
|
+
</ui-accordion-item>`
|
|
211
|
+
)
|
|
212
|
+
.join("")}
|
|
213
|
+
</ui-accordion>
|
|
214
|
+
`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { mount }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Open Privacy and security. Notification settings stays open; close either section independently.
|
|
221
|
+
|
|
222
|
+
## Disabled
|
|
223
|
+
|
|
224
|
+
Set `disabled` on an item or on a single trigger to prevent changes, or on the accordion to disable every item. Disabled triggers stay in the tab order so people can still discover them. A disabled `<fieldset>` around the accordion disables it too.
|
|
225
|
+
|
|
226
|
+
```ts title="disabled.ts"
|
|
227
|
+
import "@ui-sdk/web/accordion"
|
|
228
|
+
import { ChevronDown, createElement } from "lucide"
|
|
229
|
+
|
|
230
|
+
const items: {
|
|
231
|
+
value: string
|
|
232
|
+
label: string
|
|
233
|
+
content: string
|
|
234
|
+
disabled?: boolean
|
|
235
|
+
}[] = [
|
|
236
|
+
{
|
|
237
|
+
value: "access",
|
|
238
|
+
label: "Can I access my account history?",
|
|
239
|
+
content:
|
|
240
|
+
"Yes. Every order and payment from the last five years is in your account.",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
value: "premium",
|
|
244
|
+
label: "Premium feature information",
|
|
245
|
+
content:
|
|
246
|
+
"Premium includes priority support and early access to new features.",
|
|
247
|
+
disabled: true,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
value: "email",
|
|
251
|
+
label: "How do I update my email address?",
|
|
252
|
+
content:
|
|
253
|
+
"Open Settings, then Account, and change it there. We'll send a confirmation link.",
|
|
254
|
+
},
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
function mount(root: HTMLElement) {
|
|
258
|
+
root.innerHTML = `
|
|
259
|
+
<ui-accordion>
|
|
260
|
+
${items
|
|
261
|
+
.map(
|
|
262
|
+
(item) => `
|
|
263
|
+
<ui-accordion-item value="${item.value}"${item.disabled ? " disabled" : ""}>
|
|
264
|
+
<ui-accordion-header>
|
|
265
|
+
<h3>
|
|
266
|
+
<ui-accordion-trigger>
|
|
267
|
+
<button type="button" aria-expanded="false">
|
|
268
|
+
${item.label}
|
|
269
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
270
|
+
</button>
|
|
271
|
+
</ui-accordion-trigger>
|
|
272
|
+
</h3>
|
|
273
|
+
</ui-accordion-header>
|
|
274
|
+
<ui-accordion-panel hidden>
|
|
275
|
+
${item.content}
|
|
276
|
+
</ui-accordion-panel>
|
|
277
|
+
</ui-accordion-item>`
|
|
278
|
+
)
|
|
279
|
+
.join("")}
|
|
280
|
+
</ui-accordion>
|
|
281
|
+
`
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export { mount }
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Tab to Premium feature information. It stays focusable, and pressing it leaves its panel closed. Try “How do I update my email address?” instead.
|
|
288
|
+
|
|
289
|
+
<span id="behavior-and-options" />
|
|
290
|
+
|
|
291
|
+
## Collapsible
|
|
292
|
+
|
|
293
|
+
Set `collapsible="false"` when the open item should stay open until another item replaces it. Pair it with `default-value="overview"` so a panel starts open.
|
|
294
|
+
|
|
295
|
+
The open trigger reports `aria-disabled="true"` because pressing it leaves its panel open.
|
|
296
|
+
|
|
297
|
+
```html
|
|
298
|
+
<ui-accordion collapsible="false" default-value="overview"></ui-accordion>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```ts title="collapsible.ts"
|
|
302
|
+
import "@ui-sdk/web/accordion"
|
|
303
|
+
import { ChevronDown, createElement } from "lucide"
|
|
304
|
+
|
|
305
|
+
const items = [
|
|
306
|
+
{
|
|
307
|
+
value: "overview",
|
|
308
|
+
label: "Overview",
|
|
309
|
+
content: "A summary of the project, its goals and its owners.",
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
value: "timeline",
|
|
313
|
+
label: "Timeline",
|
|
314
|
+
content: "Kickoff in March, beta in June and launch in September.",
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
value: "budget",
|
|
318
|
+
label: "Budget",
|
|
319
|
+
content: "Approved for two engineers and one designer for six months.",
|
|
320
|
+
},
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
function mount(root: HTMLElement) {
|
|
324
|
+
root.innerHTML = `
|
|
325
|
+
<ui-accordion collapsible="false" default-value="overview">
|
|
326
|
+
${items
|
|
327
|
+
.map(
|
|
328
|
+
(item) => `
|
|
329
|
+
<ui-accordion-item value="${item.value}">
|
|
330
|
+
<ui-accordion-header>
|
|
331
|
+
<h3>
|
|
332
|
+
<ui-accordion-trigger>
|
|
333
|
+
<button type="button" aria-expanded="${item.value === "overview"}">
|
|
334
|
+
${item.label}
|
|
335
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
336
|
+
</button>
|
|
337
|
+
</ui-accordion-trigger>
|
|
338
|
+
</h3>
|
|
339
|
+
</ui-accordion-header>
|
|
340
|
+
<ui-accordion-panel${item.value === "overview" ? "" : " hidden"}>
|
|
341
|
+
${item.content}
|
|
342
|
+
</ui-accordion-panel>
|
|
343
|
+
</ui-accordion-item>`
|
|
344
|
+
)
|
|
345
|
+
.join("")}
|
|
346
|
+
</ui-accordion>
|
|
347
|
+
`
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export { mount }
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Open Timeline, then press it again. It stays open until you choose another heading.
|
|
354
|
+
|
|
355
|
+
## Controlled
|
|
356
|
+
|
|
357
|
+
Read or set the element’s `value` property to choose the open items. Setting it commits the change without firing `valuechange` or `openchange`, like a native input’s `value`; `api.setValue()` fires them like a press. Listen to `valuechange` to follow the changes people make. [State and Events](../state-and-events.md) covers the shared pattern.
|
|
358
|
+
|
|
359
|
+
An item can refuse a change: call `event.preventDefault()` in its `openchange` listener. Item `openchange` events fire before the accordion's `valuechange`, including the item that would close when another opens. Canceling any one of them cancels the whole change. `valuechange` also carries the [reason](#reasons) for the change.
|
|
360
|
+
|
|
361
|
+
```ts title="controlled.ts"
|
|
362
|
+
import "@ui-sdk/web/accordion"
|
|
363
|
+
import { ChevronDown, createElement } from "lucide"
|
|
364
|
+
|
|
365
|
+
const items = [
|
|
366
|
+
{
|
|
367
|
+
value: "account",
|
|
368
|
+
label: "Account",
|
|
369
|
+
content: "Your name, email address and profile photo.",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
value: "security",
|
|
373
|
+
label: "Security",
|
|
374
|
+
content: "Your password, passkeys and two-factor authentication.",
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
value: "sessions",
|
|
378
|
+
label: "Sessions",
|
|
379
|
+
content: "Devices signed in to your account in the last 30 days.",
|
|
380
|
+
},
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
function mount(root: HTMLElement) {
|
|
384
|
+
root.innerHTML = `
|
|
385
|
+
<div>
|
|
386
|
+
<div data-slot="controls">
|
|
387
|
+
<button type="button">Review security</button>
|
|
388
|
+
</div>
|
|
389
|
+
<ui-accordion multiple default-value="account">
|
|
390
|
+
${items
|
|
391
|
+
.map(
|
|
392
|
+
(item) => `
|
|
393
|
+
<ui-accordion-item value="${item.value}">
|
|
394
|
+
<ui-accordion-header>
|
|
395
|
+
<h3>
|
|
396
|
+
<ui-accordion-trigger>
|
|
397
|
+
<button type="button" aria-expanded="${item.value === "account"}">
|
|
398
|
+
${item.label}
|
|
399
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
400
|
+
</button>
|
|
401
|
+
</ui-accordion-trigger>
|
|
402
|
+
</h3>
|
|
403
|
+
</ui-accordion-header>
|
|
404
|
+
<ui-accordion-panel${item.value === "account" ? "" : " hidden"}>
|
|
405
|
+
${item.content}
|
|
406
|
+
</ui-accordion-panel>
|
|
407
|
+
</ui-accordion-item>`
|
|
408
|
+
)
|
|
409
|
+
.join("")}
|
|
410
|
+
</ui-accordion>
|
|
411
|
+
</div>
|
|
412
|
+
`
|
|
413
|
+
const accordion = root.querySelector("ui-accordion")!
|
|
414
|
+
root
|
|
415
|
+
.querySelector("[data-slot=controls] button")!
|
|
416
|
+
.addEventListener("click", () => accordion.api.open("security"))
|
|
417
|
+
accordion.addEventListener(
|
|
418
|
+
"openchange",
|
|
419
|
+
(event) => {
|
|
420
|
+
const open = (event as CustomEvent<{ value: boolean }>).detail.value
|
|
421
|
+
const item = (event.target as HTMLElement).getAttribute("value")
|
|
422
|
+
if (
|
|
423
|
+
!open &&
|
|
424
|
+
item === "security" &&
|
|
425
|
+
!window.confirm("Close the security settings?")
|
|
426
|
+
) {
|
|
427
|
+
event.preventDefault()
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
{ capture: true }
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export { mount }
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Press Security to open it, then press it again. Cancel the confirmation and Security stays open; accept it and the section closes.
|
|
438
|
+
|
|
439
|
+
## Keep Mounted
|
|
440
|
+
|
|
441
|
+
A closed panel normally unmounts. Add `keep-mounted` to one panel or the whole accordion to keep its content hidden in the page. This preserves local state, such as a draft in a field.
|
|
442
|
+
|
|
443
|
+
```ts title="keep-mounted.ts"
|
|
444
|
+
import "@ui-sdk/web/accordion"
|
|
445
|
+
import "@ui-sdk/web/field"
|
|
446
|
+
import "@ui-sdk/web/textarea"
|
|
447
|
+
import { ChevronDown, createElement } from "lucide"
|
|
448
|
+
|
|
449
|
+
function mount(root: HTMLElement) {
|
|
450
|
+
root.innerHTML = `
|
|
451
|
+
<ui-accordion default-value="reply">
|
|
452
|
+
<ui-accordion-item value="reply">
|
|
453
|
+
<ui-accordion-header>
|
|
454
|
+
<h3>
|
|
455
|
+
<ui-accordion-trigger>
|
|
456
|
+
<button type="button" aria-expanded="true">
|
|
457
|
+
Reply to this thread
|
|
458
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
459
|
+
</button>
|
|
460
|
+
</ui-accordion-trigger>
|
|
461
|
+
</h3>
|
|
462
|
+
</ui-accordion-header>
|
|
463
|
+
<ui-accordion-panel keep-mounted>
|
|
464
|
+
<ui-field>
|
|
465
|
+
<ui-field-label>Message</ui-field-label>
|
|
466
|
+
<ui-textarea>
|
|
467
|
+
<textarea rows="3" placeholder="Write a reply…"></textarea>
|
|
468
|
+
</ui-textarea>
|
|
469
|
+
</ui-field>
|
|
470
|
+
</ui-accordion-panel>
|
|
471
|
+
</ui-accordion-item>
|
|
472
|
+
<ui-accordion-item value="formatting">
|
|
473
|
+
<ui-accordion-header>
|
|
474
|
+
<h3>
|
|
475
|
+
<ui-accordion-trigger>
|
|
476
|
+
<button type="button" aria-expanded="false">
|
|
477
|
+
Formatting
|
|
478
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
479
|
+
</button>
|
|
480
|
+
</ui-accordion-trigger>
|
|
481
|
+
</h3>
|
|
482
|
+
</ui-accordion-header>
|
|
483
|
+
<ui-accordion-panel hidden>
|
|
484
|
+
<p>Use **bold**, _italics_ and \`code\`. Links paste as previews.</p>
|
|
485
|
+
</ui-accordion-panel>
|
|
486
|
+
</ui-accordion-item>
|
|
487
|
+
</ui-accordion>
|
|
488
|
+
`
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export { mount }
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Write a message, switch to Formatting, then return to Reply to this thread. Your draft is still there.
|
|
495
|
+
|
|
496
|
+
## Find in Page
|
|
497
|
+
|
|
498
|
+
Add `hidden-until-found` to keep closed panels available to the browser's Find command. A match opens the item immediately, without an animation, with the [reason](#reasons) `find-in-page`.
|
|
499
|
+
|
|
500
|
+
```ts title="find-in-page.ts"
|
|
501
|
+
import "@ui-sdk/web/accordion"
|
|
502
|
+
import { ChevronDown, createElement } from "lucide"
|
|
503
|
+
|
|
504
|
+
const items = [
|
|
505
|
+
{
|
|
506
|
+
value: "fruit",
|
|
507
|
+
label: "Which fruit do you stock?",
|
|
508
|
+
content: "Apples, pears, mangoes and, in summer, pineapple.",
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
value: "organic",
|
|
512
|
+
label: "Is it organic?",
|
|
513
|
+
content: "Everything we stock is certified organic.",
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
value: "delivery",
|
|
517
|
+
label: "When do you deliver?",
|
|
518
|
+
content: "Every weekday morning before 10.",
|
|
519
|
+
},
|
|
520
|
+
]
|
|
521
|
+
|
|
522
|
+
function mount(root: HTMLElement) {
|
|
523
|
+
root.innerHTML = `
|
|
524
|
+
<ui-accordion hidden-until-found>
|
|
525
|
+
${items
|
|
526
|
+
.map(
|
|
527
|
+
(item) => `
|
|
528
|
+
<ui-accordion-item value="${item.value}">
|
|
529
|
+
<ui-accordion-header>
|
|
530
|
+
<h3>
|
|
531
|
+
<ui-accordion-trigger>
|
|
532
|
+
<button type="button" aria-expanded="false">
|
|
533
|
+
${item.label}
|
|
534
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
535
|
+
</button>
|
|
536
|
+
</ui-accordion-trigger>
|
|
537
|
+
</h3>
|
|
538
|
+
</ui-accordion-header>
|
|
539
|
+
<ui-accordion-panel hidden="until-found">
|
|
540
|
+
${item.content}
|
|
541
|
+
</ui-accordion-panel>
|
|
542
|
+
</ui-accordion-item>`
|
|
543
|
+
)
|
|
544
|
+
.join("")}
|
|
545
|
+
</ui-accordion>
|
|
546
|
+
`
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export { mount }
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
Use your browser’s Find command to search for “pineapple”. The closed “Which fruit do you stock?” panel opens to reveal the match.
|
|
553
|
+
|
|
554
|
+
<span id="deep-links" />
|
|
555
|
+
|
|
556
|
+
A link to an element inside a closed panel can reveal it too. Give the target an `id` and link to it with a hash, such as `href="#refund-timeline"`.
|
|
557
|
+
|
|
558
|
+
```ts title="deep-link.ts"
|
|
559
|
+
import "@ui-sdk/web/accordion"
|
|
560
|
+
import { ChevronDown, createElement } from "lucide"
|
|
561
|
+
|
|
562
|
+
const items = [
|
|
563
|
+
{
|
|
564
|
+
value: "returns",
|
|
565
|
+
label: "Returns",
|
|
566
|
+
content: `
|
|
567
|
+
<p>Return any unused item in its original packaging within 30 days.</p>
|
|
568
|
+
<p id="return-label">To print a return label, open the order and choose Return. The label is free.</p>`,
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
value: "refunds",
|
|
572
|
+
label: "Refunds",
|
|
573
|
+
content: `
|
|
574
|
+
<p>Refunds go back to the card you paid with.</p>
|
|
575
|
+
<p id="refund-timeline">Timeline: we inspect the item within 2 days of receiving it, and your bank shows the refund 5 to 7 business days later.</p>`,
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
value: "exchanges",
|
|
579
|
+
label: "Exchanges",
|
|
580
|
+
content: `
|
|
581
|
+
<p>Start a return, choose Exchange and pick the new size. Shipping both ways is free.</p>`,
|
|
582
|
+
},
|
|
583
|
+
]
|
|
584
|
+
|
|
585
|
+
function mount(root: HTMLElement) {
|
|
586
|
+
root.innerHTML = `
|
|
587
|
+
<div>
|
|
588
|
+
<nav data-slot="controls" aria-label="Jump to">
|
|
589
|
+
<a href="#refund-timeline">Jump to the refund timeline</a>
|
|
590
|
+
<a href="#return-label">Print a return label</a>
|
|
591
|
+
</nav>
|
|
592
|
+
<ui-accordion hidden-until-found>
|
|
593
|
+
${items
|
|
594
|
+
.map(
|
|
595
|
+
(item) => `
|
|
596
|
+
<ui-accordion-item value="${item.value}">
|
|
597
|
+
<ui-accordion-header>
|
|
598
|
+
<h3>
|
|
599
|
+
<ui-accordion-trigger>
|
|
600
|
+
<button type="button" aria-expanded="false">
|
|
601
|
+
${item.label}
|
|
602
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
603
|
+
</button>
|
|
604
|
+
</ui-accordion-trigger>
|
|
605
|
+
</h3>
|
|
606
|
+
</ui-accordion-header>
|
|
607
|
+
<ui-accordion-panel hidden="until-found">
|
|
608
|
+
<div data-slot="stack">${item.content}</div>
|
|
609
|
+
</ui-accordion-panel>
|
|
610
|
+
</ui-accordion-item>`
|
|
611
|
+
)
|
|
612
|
+
.join("")}
|
|
613
|
+
</ui-accordion>
|
|
614
|
+
</div>
|
|
615
|
+
`
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export { mount }
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
Press Jump to the refund timeline. The closed Refunds panel opens and the browser moves to the linked paragraph.
|
|
622
|
+
|
|
623
|
+
With `hidden-until-found`, write `hidden="until-found"` on each closed `ui-accordion-panel` so it is findable before the elements upgrade. For an initial URL hash, [open the item explicitly](#url-hash).
|
|
624
|
+
|
|
625
|
+
## Animation
|
|
626
|
+
|
|
627
|
+
Each part has a `data-slot`, such as `accordion-panel`. The panel exposes its measured height as `--accordion-panel-height`. Animate that height to or from zero with `data-starting-style` and `data-ending-style`:
|
|
628
|
+
|
|
629
|
+
```css
|
|
630
|
+
[data-slot="accordion-panel"] {
|
|
631
|
+
height: var(--accordion-panel-height);
|
|
632
|
+
overflow: hidden;
|
|
633
|
+
transition: height 0.15s;
|
|
634
|
+
|
|
635
|
+
&:is([data-starting-style], [data-ending-style]) {
|
|
636
|
+
height: 0;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
The panel stays mounted until its exit animation finishes. An item's `openchangecomplete` fires afterward with the open state. A panel that starts open appears right away, with no animation and no completion callback.
|
|
642
|
+
|
|
643
|
+
```ts title="animation.ts"
|
|
644
|
+
import "@ui-sdk/web/accordion"
|
|
645
|
+
import { ChevronDown, createElement } from "lucide"
|
|
646
|
+
|
|
647
|
+
const items = [
|
|
648
|
+
{
|
|
649
|
+
value: "one",
|
|
650
|
+
label: "Why animate the height?",
|
|
651
|
+
content: "So the content below slides instead of jumping.",
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
value: "two",
|
|
655
|
+
label: "Where does the height come from?",
|
|
656
|
+
content:
|
|
657
|
+
"The panel measures itself and writes the result to a CSS variable while it animates.",
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
value: "three",
|
|
661
|
+
label: "Does it animate on first paint?",
|
|
662
|
+
content: "No. Panels open on the server or on mount show at once.",
|
|
663
|
+
},
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
function mount(root: HTMLElement) {
|
|
667
|
+
root.innerHTML = `
|
|
668
|
+
<ui-accordion default-value="one">
|
|
669
|
+
${items
|
|
670
|
+
.map(
|
|
671
|
+
(item) => `
|
|
672
|
+
<ui-accordion-item value="${item.value}">
|
|
673
|
+
<ui-accordion-header>
|
|
674
|
+
<h3>
|
|
675
|
+
<ui-accordion-trigger>
|
|
676
|
+
<button type="button" aria-expanded="${item.value === "one"}">
|
|
677
|
+
${item.label}
|
|
678
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
679
|
+
</button>
|
|
680
|
+
</ui-accordion-trigger>
|
|
681
|
+
</h3>
|
|
682
|
+
</ui-accordion-header>
|
|
683
|
+
<ui-accordion-panel${item.value === "one" ? "" : " hidden"}>
|
|
684
|
+
${item.content}
|
|
685
|
+
</ui-accordion-panel>
|
|
686
|
+
</ui-accordion-item>`
|
|
687
|
+
)
|
|
688
|
+
.join("")}
|
|
689
|
+
</ui-accordion>
|
|
690
|
+
`
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export { mount }
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
Open or close an item. Its panel animates its height.
|
|
697
|
+
|
|
698
|
+
## Expand All
|
|
699
|
+
|
|
700
|
+
With `multiple`, the element's `api.openAll()` opens every enabled item. `closeAll()` closes every item that may close. The element's `api` property is its controller; [Controllers](../controllers.md#use-the-elements-controller) shows how to use it.
|
|
701
|
+
|
|
702
|
+
```ts title="expand-all.ts"
|
|
703
|
+
import type { AccordionController } from "@ui-sdk/core/accordion"
|
|
704
|
+
import "@ui-sdk/web/accordion"
|
|
705
|
+
import { ChevronDown, createElement } from "lucide"
|
|
706
|
+
|
|
707
|
+
const weeks = [
|
|
708
|
+
{
|
|
709
|
+
value: "week-1",
|
|
710
|
+
label: "Week 1: Foundations",
|
|
711
|
+
content:
|
|
712
|
+
"Set up your editor, meet the command line and write your first script.",
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
value: "week-2",
|
|
716
|
+
label: "Week 2: Data and types",
|
|
717
|
+
content:
|
|
718
|
+
"Strings, numbers, lists and objects, and how to tell them apart when a program runs.",
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
value: "week-3",
|
|
722
|
+
label: "Week 3: Control flow",
|
|
723
|
+
content:
|
|
724
|
+
"Conditions, loops and functions. The project this week is a small text adventure.",
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
value: "week-4",
|
|
728
|
+
label: "Week 4: Working with files",
|
|
729
|
+
content:
|
|
730
|
+
"Read and write files, parse CSV and JSON, and handle the errors that come with them.",
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
value: "week-5",
|
|
734
|
+
label: "Week 5: Final project",
|
|
735
|
+
content:
|
|
736
|
+
"Build a tool you'll use yourself, then present it to the group in a five-minute demo.",
|
|
737
|
+
},
|
|
738
|
+
]
|
|
739
|
+
|
|
740
|
+
type OpenChange = CustomEvent<{ value: boolean; reason: string }>
|
|
741
|
+
|
|
742
|
+
function mount(root: HTMLElement) {
|
|
743
|
+
root.innerHTML = `
|
|
744
|
+
<div>
|
|
745
|
+
<div data-slot="controls">
|
|
746
|
+
<button type="button" data-action="expand">Expand all</button>
|
|
747
|
+
<button type="button" data-action="collapse">Collapse all</button>
|
|
748
|
+
</div>
|
|
749
|
+
<div data-slot="scroll">
|
|
750
|
+
<ui-accordion multiple>
|
|
751
|
+
${weeks
|
|
752
|
+
.map(
|
|
753
|
+
(week) => `
|
|
754
|
+
<ui-accordion-item value="${week.value}">
|
|
755
|
+
<ui-accordion-header>
|
|
756
|
+
<h3>
|
|
757
|
+
<ui-accordion-trigger>
|
|
758
|
+
<button type="button" aria-expanded="false">
|
|
759
|
+
${week.label}
|
|
760
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
761
|
+
</button>
|
|
762
|
+
</ui-accordion-trigger>
|
|
763
|
+
</h3>
|
|
764
|
+
</ui-accordion-header>
|
|
765
|
+
<ui-accordion-panel hidden>
|
|
766
|
+
${week.content}
|
|
767
|
+
</ui-accordion-panel>
|
|
768
|
+
</ui-accordion-item>`
|
|
769
|
+
)
|
|
770
|
+
.join("")}
|
|
771
|
+
</ui-accordion>
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
774
|
+
`
|
|
775
|
+
const syllabus = root.querySelector("ui-accordion") as HTMLElement & {
|
|
776
|
+
api: AccordionController
|
|
777
|
+
}
|
|
778
|
+
root
|
|
779
|
+
.querySelector("[data-action=expand]")!
|
|
780
|
+
.addEventListener("click", () => syllabus.api.openAll())
|
|
781
|
+
root
|
|
782
|
+
.querySelector("[data-action=collapse]")!
|
|
783
|
+
.addEventListener("click", () => syllabus.api.closeAll())
|
|
784
|
+
|
|
785
|
+
let pressed: EventTarget | null = null
|
|
786
|
+
syllabus.addEventListener(
|
|
787
|
+
"openchange",
|
|
788
|
+
(event) => {
|
|
789
|
+
const { value: open, reason } = (event as OpenChange).detail
|
|
790
|
+
if (open && reason === "trigger-press") {
|
|
791
|
+
pressed = event.target
|
|
792
|
+
} else if (pressed === event.target) {
|
|
793
|
+
pressed = null
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
{ capture: true }
|
|
797
|
+
)
|
|
798
|
+
syllabus.addEventListener(
|
|
799
|
+
"openchangecomplete",
|
|
800
|
+
(event) => {
|
|
801
|
+
const { value: open } = (event as OpenChange).detail
|
|
802
|
+
const week = event.target as HTMLElement
|
|
803
|
+
if (!open || pressed !== week) {
|
|
804
|
+
return
|
|
805
|
+
}
|
|
806
|
+
pressed = null
|
|
807
|
+
week.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
|
808
|
+
},
|
|
809
|
+
{ capture: true }
|
|
810
|
+
)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export { mount }
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Press Expand all or Collapse all. Open an individual week near the bottom to scroll it into view after its panel opens.
|
|
817
|
+
|
|
818
|
+
## Nested
|
|
819
|
+
|
|
820
|
+
Put another accordion inside a panel when a section has its own expandable sections. Each accordion tracks its own items. Give the inner headings a lower level than the outer headings.
|
|
821
|
+
|
|
822
|
+
```ts title="nested.ts"
|
|
823
|
+
import "@ui-sdk/web/accordion"
|
|
824
|
+
import { ChevronDown, createElement } from "lucide"
|
|
825
|
+
|
|
826
|
+
const sections: {
|
|
827
|
+
value: string
|
|
828
|
+
label: string
|
|
829
|
+
content?: string
|
|
830
|
+
topics?: { value: string; label: string; content: string }[]
|
|
831
|
+
}[] = [
|
|
832
|
+
{
|
|
833
|
+
value: "profile",
|
|
834
|
+
label: "Profile",
|
|
835
|
+
content: "Your name, photo and the email address people see.",
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
value: "notifications",
|
|
839
|
+
label: "Notifications",
|
|
840
|
+
topics: [
|
|
841
|
+
{
|
|
842
|
+
value: "email",
|
|
843
|
+
label: "Email",
|
|
844
|
+
content: "A weekly digest, and mentions as they happen.",
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
value: "push",
|
|
848
|
+
label: "Push",
|
|
849
|
+
content: "Direct messages and replies to your comments.",
|
|
850
|
+
},
|
|
851
|
+
],
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
value: "billing",
|
|
855
|
+
label: "Billing",
|
|
856
|
+
topics: [
|
|
857
|
+
{
|
|
858
|
+
value: "payment",
|
|
859
|
+
label: "Payment methods",
|
|
860
|
+
content: "A Visa card ending in 4242, used for every renewal.",
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
value: "invoices",
|
|
864
|
+
label: "Invoices",
|
|
865
|
+
content: "Download a PDF of any invoice from the last two years.",
|
|
866
|
+
},
|
|
867
|
+
],
|
|
868
|
+
},
|
|
869
|
+
]
|
|
870
|
+
|
|
871
|
+
const topics = (list: { value: string; label: string; content: string }[]) => `
|
|
872
|
+
<ui-accordion multiple>
|
|
873
|
+
${list
|
|
874
|
+
.map(
|
|
875
|
+
(topic) => `
|
|
876
|
+
<ui-accordion-item value="${topic.value}">
|
|
877
|
+
<ui-accordion-header>
|
|
878
|
+
<h4>
|
|
879
|
+
<ui-accordion-trigger>
|
|
880
|
+
<button type="button" aria-expanded="false">
|
|
881
|
+
${topic.label}
|
|
882
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
883
|
+
</button>
|
|
884
|
+
</ui-accordion-trigger>
|
|
885
|
+
</h4>
|
|
886
|
+
</ui-accordion-header>
|
|
887
|
+
<ui-accordion-panel hidden>
|
|
888
|
+
${topic.content}
|
|
889
|
+
</ui-accordion-panel>
|
|
890
|
+
</ui-accordion-item>`
|
|
891
|
+
)
|
|
892
|
+
.join("")}
|
|
893
|
+
</ui-accordion>`
|
|
894
|
+
|
|
895
|
+
function mount(root: HTMLElement) {
|
|
896
|
+
root.innerHTML = `
|
|
897
|
+
<ui-accordion default-value="notifications">
|
|
898
|
+
${sections
|
|
899
|
+
.map(
|
|
900
|
+
(section) => `
|
|
901
|
+
<ui-accordion-item value="${section.value}">
|
|
902
|
+
<ui-accordion-header>
|
|
903
|
+
<h3>
|
|
904
|
+
<ui-accordion-trigger>
|
|
905
|
+
<button type="button" aria-expanded="${section.value === "notifications"}">
|
|
906
|
+
${section.label}
|
|
907
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
908
|
+
</button>
|
|
909
|
+
</ui-accordion-trigger>
|
|
910
|
+
</h3>
|
|
911
|
+
</ui-accordion-header>
|
|
912
|
+
<ui-accordion-panel${section.value === "notifications" ? "" : " hidden"}>
|
|
913
|
+
${section.topics ? topics(section.topics) : `<div>${section.content}</div>`}
|
|
914
|
+
</ui-accordion-panel>
|
|
915
|
+
</ui-accordion-item>`
|
|
916
|
+
)
|
|
917
|
+
.join("")}
|
|
918
|
+
</ui-accordion>
|
|
919
|
+
`
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
export { mount }
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
Open Billing, then Invoices inside it. The inner accordion changes without closing Billing.
|
|
926
|
+
|
|
927
|
+
## FAQ Search
|
|
928
|
+
|
|
929
|
+
<span id="faq-with-search" />
|
|
930
|
+
|
|
931
|
+
Combine a search field with a controlled accordion: filter the questions and set `value` to the matching items. The example also highlights matches and leaves the triggers usable while the search is active.
|
|
932
|
+
|
|
933
|
+
```ts title="search.ts"
|
|
934
|
+
import "@ui-sdk/web/accordion"
|
|
935
|
+
import { ChevronDown, createElement } from "lucide"
|
|
936
|
+
|
|
937
|
+
const faq = [
|
|
938
|
+
{
|
|
939
|
+
value: "returns",
|
|
940
|
+
question: "What is your return policy?",
|
|
941
|
+
answer:
|
|
942
|
+
"Return any unused item within 30 days. Once it reaches us, your refund is on its way.",
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
value: "refunds",
|
|
946
|
+
question: "How long do refunds take?",
|
|
947
|
+
answer:
|
|
948
|
+
"Refunds reach your card within 5 to 7 business days after we receive the item.",
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
value: "shipping",
|
|
952
|
+
question: "Do you ship internationally?",
|
|
953
|
+
answer:
|
|
954
|
+
"We ship to 40 countries. Duties are included in the price at checkout.",
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
value: "tracking",
|
|
958
|
+
question: "How do I track my order?",
|
|
959
|
+
answer:
|
|
960
|
+
"Every shipping email has a tracking link. You'll also find it under Orders.",
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
value: "exchange",
|
|
964
|
+
question: "Can I exchange an item for another size?",
|
|
965
|
+
answer:
|
|
966
|
+
"Yes. Start a return, choose Exchange and pick the new size. Shipping is free.",
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
value: "gift",
|
|
970
|
+
question: "Can I send an order as a gift?",
|
|
971
|
+
answer:
|
|
972
|
+
"Add a gift note at checkout and we'll leave the prices off the packing slip.",
|
|
973
|
+
},
|
|
974
|
+
]
|
|
975
|
+
|
|
976
|
+
function matches(question: (typeof faq)[number], query: string) {
|
|
977
|
+
const search = query.trim().toLowerCase()
|
|
978
|
+
return (
|
|
979
|
+
!search ||
|
|
980
|
+
question.question.toLowerCase().includes(search) ||
|
|
981
|
+
question.answer.toLowerCase().includes(search)
|
|
982
|
+
)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function highlight(text: string, query: string) {
|
|
986
|
+
const search = query.trim().toLowerCase()
|
|
987
|
+
if (!search) {
|
|
988
|
+
return [{ text, match: false }]
|
|
989
|
+
}
|
|
990
|
+
const parts: { text: string; match: boolean }[] = []
|
|
991
|
+
let start = 0
|
|
992
|
+
for (let index = text.toLowerCase().indexOf(search); index !== -1;) {
|
|
993
|
+
if (index > start) {
|
|
994
|
+
parts.push({ text: text.slice(start, index), match: false })
|
|
995
|
+
}
|
|
996
|
+
parts.push({ text: text.slice(index, index + search.length), match: true })
|
|
997
|
+
start = index + search.length
|
|
998
|
+
index = text.toLowerCase().indexOf(search, start)
|
|
999
|
+
}
|
|
1000
|
+
if (start < text.length) {
|
|
1001
|
+
parts.push({ text: text.slice(start), match: false })
|
|
1002
|
+
}
|
|
1003
|
+
return parts
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const escape = (text: string) =>
|
|
1007
|
+
text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`)
|
|
1008
|
+
|
|
1009
|
+
const highlighted = (text: string, query: string) =>
|
|
1010
|
+
highlight(text, query)
|
|
1011
|
+
.map((part) =>
|
|
1012
|
+
part.match ? `<mark>${escape(part.text)}</mark>` : escape(part.text)
|
|
1013
|
+
)
|
|
1014
|
+
.join("")
|
|
1015
|
+
|
|
1016
|
+
function mount(root: HTMLElement) {
|
|
1017
|
+
root.innerHTML = `
|
|
1018
|
+
<div>
|
|
1019
|
+
<div data-slot="controls">
|
|
1020
|
+
<input type="search" aria-label="Search questions" placeholder="Search questions…" />
|
|
1021
|
+
</div>
|
|
1022
|
+
<ui-accordion multiple>
|
|
1023
|
+
${faq
|
|
1024
|
+
.map(
|
|
1025
|
+
(question) => `
|
|
1026
|
+
<ui-accordion-item value="${question.value}">
|
|
1027
|
+
<ui-accordion-header>
|
|
1028
|
+
<h3>
|
|
1029
|
+
<ui-accordion-trigger>
|
|
1030
|
+
<button type="button" aria-expanded="false">
|
|
1031
|
+
<span>${question.question}</span>
|
|
1032
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
1033
|
+
</button>
|
|
1034
|
+
</ui-accordion-trigger>
|
|
1035
|
+
</h3>
|
|
1036
|
+
</ui-accordion-header>
|
|
1037
|
+
<ui-accordion-panel hidden>
|
|
1038
|
+
${question.answer}
|
|
1039
|
+
</ui-accordion-panel>
|
|
1040
|
+
</ui-accordion-item>`
|
|
1041
|
+
)
|
|
1042
|
+
.join("")}
|
|
1043
|
+
</ui-accordion>
|
|
1044
|
+
<p data-slot="hint" role="status"></p>
|
|
1045
|
+
</div>
|
|
1046
|
+
`
|
|
1047
|
+
const accordion = root.querySelector("ui-accordion")!
|
|
1048
|
+
const status = root.querySelector("[role=status]")!
|
|
1049
|
+
root.querySelector("input")!.addEventListener("input", (event) => {
|
|
1050
|
+
const query = (event.target as HTMLInputElement).value
|
|
1051
|
+
const results = faq.filter((question) => matches(question, query))
|
|
1052
|
+
for (const question of faq) {
|
|
1053
|
+
const element = accordion.querySelector<HTMLElement>(
|
|
1054
|
+
`ui-accordion-item[value="${question.value}"]`
|
|
1055
|
+
)!
|
|
1056
|
+
element.hidden = !results.includes(question)
|
|
1057
|
+
element.querySelector("button > span")!.innerHTML = highlighted(
|
|
1058
|
+
question.question,
|
|
1059
|
+
query
|
|
1060
|
+
)
|
|
1061
|
+
element.querySelector("ui-accordion-panel")!.innerHTML = highlighted(
|
|
1062
|
+
question.answer,
|
|
1063
|
+
query
|
|
1064
|
+
)
|
|
1065
|
+
}
|
|
1066
|
+
accordion.value = query.trim()
|
|
1067
|
+
? results.map((question) => question.value)
|
|
1068
|
+
: []
|
|
1069
|
+
status.textContent = results.length ? "" : `No questions match “${query}”.`
|
|
1070
|
+
})
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
export { mount }
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
Search for “refund” to show and open matching answers. Try “zzz” for the empty result, then clear the search to restore all questions.
|
|
1077
|
+
|
|
1078
|
+
## URL Hash
|
|
1079
|
+
|
|
1080
|
+
<span id="open-the-item-in-the-url-hash" />
|
|
1081
|
+
|
|
1082
|
+
A hash can name an item's `value` instead of an element inside its panel. Read it after mount and on `hashchange`, then open and focus the matching item through your controller:
|
|
1083
|
+
|
|
1084
|
+
```ts
|
|
1085
|
+
const value = decodeURIComponent(location.hash.slice(1))
|
|
1086
|
+
if (items.some((item) => item.value === value)) {
|
|
1087
|
+
accordion.api.open(value)
|
|
1088
|
+
accordion.api.focus(value)
|
|
1089
|
+
}
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
Check the value against your items first. A controller can hold a value even when no item renders it. Remove the `hashchange` listener when the component unmounts.
|
|
1093
|
+
|
|
1094
|
+
## Query String
|
|
1095
|
+
|
|
1096
|
+
<span id="keep-open-items-in-the-query-string" />
|
|
1097
|
+
|
|
1098
|
+
Use `open` query parameters when several items should be shareable. Read the initial values from your router on the server, then write accepted changes back to the URL:
|
|
1099
|
+
|
|
1100
|
+
```ts
|
|
1101
|
+
function saveOpenItems(value: string[]) {
|
|
1102
|
+
const url = new URL(location.href)
|
|
1103
|
+
url.searchParams.delete("open")
|
|
1104
|
+
for (const item of value) url.searchParams.append("open", item)
|
|
1105
|
+
history.replaceState(null, "", url)
|
|
1106
|
+
}
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
Call this with `event.detail.value` from a `valuechange` listener. If your app already has a router, use its navigation API instead.
|
|
1110
|
+
|
|
1111
|
+
## Scroll into View
|
|
1112
|
+
|
|
1113
|
+
<span id="scroll-an-item-into-view" />
|
|
1114
|
+
|
|
1115
|
+
Scroll after `openchangecomplete`, when the panel has its full height. Scrolling in `openchange` would measure it before it opens. The [Expand All example](#expand-all) shows the full setup and scrolls only when someone opens a single item.
|
|
1116
|
+
|
|
1117
|
+
## Custom Rendering
|
|
1118
|
+
|
|
1119
|
+
<span id="render-functions" />
|
|
1120
|
+
|
|
1121
|
+
Write the heading level you need inside `ui-accordion-header`. Other parts can bind your element with `as-child`. [Parts and Composition](../parts-and-composition.md#choose-the-element) covers custom elements and part state. Accordion's states are listed under [State](#state).
|
|
1122
|
+
|
|
1123
|
+
```ts title="render.ts"
|
|
1124
|
+
import "@ui-sdk/web/accordion"
|
|
1125
|
+
|
|
1126
|
+
const steps = [
|
|
1127
|
+
{
|
|
1128
|
+
value: "install",
|
|
1129
|
+
label: "Install the package",
|
|
1130
|
+
content: "Add it with your package manager. It has no dependencies.",
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
value: "import",
|
|
1134
|
+
label: "Import the parts",
|
|
1135
|
+
content: "Import the accordion parts you need from the package.",
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
value: "style",
|
|
1139
|
+
label: "Style it",
|
|
1140
|
+
content: "Every part renders its state as data attributes you can style.",
|
|
1141
|
+
},
|
|
1142
|
+
]
|
|
1143
|
+
|
|
1144
|
+
export function mount(root: HTMLElement) {
|
|
1145
|
+
root.innerHTML = `
|
|
1146
|
+
<ui-accordion multiple default-value="install">
|
|
1147
|
+
${steps
|
|
1148
|
+
.map(
|
|
1149
|
+
(step, index) => `
|
|
1150
|
+
<ui-accordion-item value="${step.value}">
|
|
1151
|
+
<ui-accordion-header>
|
|
1152
|
+
<h4>
|
|
1153
|
+
<ui-accordion-trigger>
|
|
1154
|
+
<button type="button" aria-expanded="${step.value === "install"}">
|
|
1155
|
+
<span data-slot="hint" aria-hidden="true">${String(index + 1).padStart(2, "0")}</span>
|
|
1156
|
+
<span>${step.label}</span>
|
|
1157
|
+
<span data-slot="hint" aria-hidden="true" data-show="closed">Show</span>
|
|
1158
|
+
<span data-slot="hint" aria-hidden="true" data-show="open">Hide</span>
|
|
1159
|
+
</button>
|
|
1160
|
+
</ui-accordion-trigger>
|
|
1161
|
+
</h4>
|
|
1162
|
+
</ui-accordion-header>
|
|
1163
|
+
<ui-accordion-panel${step.value === "install" ? "" : " hidden"}>
|
|
1164
|
+
${step.content}
|
|
1165
|
+
</ui-accordion-panel>
|
|
1166
|
+
</ui-accordion-item>`
|
|
1167
|
+
)
|
|
1168
|
+
.join("")}
|
|
1169
|
+
</ui-accordion>
|
|
1170
|
+
`
|
|
1171
|
+
}
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
Each step is an h4 with its number and a Show or Hide hint that follows its open state. Open Import the parts to see it update.
|
|
1175
|
+
|
|
1176
|
+
## Styling
|
|
1177
|
+
|
|
1178
|
+
Style the parts with the attributes below. This turns the chevron as its panel opens, mirrors it in right to left with `:dir(rtl)`, and dims disabled triggers:
|
|
1179
|
+
|
|
1180
|
+
```css
|
|
1181
|
+
[data-slot="accordion-trigger"] {
|
|
1182
|
+
& > svg {
|
|
1183
|
+
rotate: -90deg;
|
|
1184
|
+
|
|
1185
|
+
&:dir(rtl) {
|
|
1186
|
+
rotate: 90deg;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
&[data-open] > svg {
|
|
1191
|
+
rotate: 0deg;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
&[data-disabled] {
|
|
1195
|
+
color: gray;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
### `<ui-accordion-panel>`
|
|
1201
|
+
- `--accordion-panel-height`: The panel's content height in pixels while it animates, auto once open and 0px while closed.
|
|
1202
|
+
- `--accordion-panel-width`: The panel's content width, the same way. For accordions that open sideways.
|
|
1203
|
+
|
|
1204
|
+
## Data Attributes
|
|
1205
|
+
|
|
1206
|
+
### `<ui-accordion>`
|
|
1207
|
+
`[data-slot="accordion"]`
|
|
1208
|
+
|
|
1209
|
+
- `data-disabled`: Present when the whole accordion is disabled.
|
|
1210
|
+
|
|
1211
|
+
### `<ui-accordion-item>`
|
|
1212
|
+
`[data-slot="accordion-item"]`
|
|
1213
|
+
|
|
1214
|
+
- `data-open`: Present while the item is open.
|
|
1215
|
+
- `data-closed`: Present while the item is closed, including while its panel animates out.
|
|
1216
|
+
- `data-disabled`: Present when the item can't open or close.
|
|
1217
|
+
|
|
1218
|
+
### `<ui-accordion-header>`
|
|
1219
|
+
`[data-slot="accordion-header"]`
|
|
1220
|
+
|
|
1221
|
+
- `data-open`: Present while the item is open.
|
|
1222
|
+
- `data-closed`: Present while the item is closed, including while its panel animates out.
|
|
1223
|
+
- `data-disabled`: Present when the item can't open or close.
|
|
1224
|
+
|
|
1225
|
+
### `<ui-accordion-trigger>`
|
|
1226
|
+
`[data-slot="accordion-trigger"]`
|
|
1227
|
+
|
|
1228
|
+
- `data-open`: Present while the item is open.
|
|
1229
|
+
- `data-closed`: Present while the item is closed, including while its panel animates out.
|
|
1230
|
+
- `data-disabled`: Present when the item can't open or close.
|
|
1231
|
+
|
|
1232
|
+
### `<ui-accordion-panel>`
|
|
1233
|
+
`[data-slot="accordion-panel"]`
|
|
1234
|
+
|
|
1235
|
+
- `data-open`: Present while the item is open.
|
|
1236
|
+
- `data-closed`: Present while the item is closed, including while its panel animates out.
|
|
1237
|
+
- `data-disabled`: Present when the item can't open or close.
|
|
1238
|
+
- `data-starting-style`: Present for a panel's first frame. Style the state you animate from.
|
|
1239
|
+
- `data-ending-style`: Present while a panel animates closed. Style the state you animate to.
|
|
1240
|
+
|
|
1241
|
+
## API Reference
|
|
1242
|
+
|
|
1243
|
+
Props are attributes in kebab case, like `default-value` (open values separated by commas) and
|
|
1244
|
+
`hidden-until-found`. Attributes can change at any time. Callbacks are events.
|
|
1245
|
+
|
|
1246
|
+
### Accordion
|
|
1247
|
+
|
|
1248
|
+
- `value?` (`T[]`): The open items' values. Pass it to control the accordion.
|
|
1249
|
+
- `defaultValue?` (`T[]`): The items open at first, when uncontrolled.
|
|
1250
|
+
- `valuechange event`: Runs when items want to open or close. `details.reason` says why, and `details.cancel()` stops it.
|
|
1251
|
+
- `multiple?` (`boolean`): Whether several items can be open at once. Otherwise opening one closes the others.
|
|
1252
|
+
- `collapsible?` (`boolean`): Whether the open item can close when only one may be open. With `false`, one item always stays open and its trigger reports it can't be pressed.
|
|
1253
|
+
- `disabled?` (`boolean`): Stops every item from opening or closing. Triggers stay focusable.
|
|
1254
|
+
- `keepMounted?` (`boolean`): Whether closed panels stay in the page, hidden.
|
|
1255
|
+
- `hiddenUntilFound?` (`boolean`): Keeps closed panels in the page as `hidden="until-found"`, so the browser's find in page can match their text and open them.
|
|
1256
|
+
- `controller?` (`object`): Opens and closes items from anywhere. Create one with `createAccordionController()`.
|
|
1257
|
+
- `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
|
|
1258
|
+
|
|
1259
|
+
### AccordionItem
|
|
1260
|
+
|
|
1261
|
+
- `disabled?` (`boolean`): Stops the item from opening or closing. Its trigger stays focusable.
|
|
1262
|
+
- `openchange event`: Runs before the item opens or closes, before the accordion's `valuechange`. `details.cancel()` stops it.
|
|
1263
|
+
- `openchangecomplete event`: Runs once the item's panel has finished opening or closing, after any animation.
|
|
1264
|
+
- `value?` (`T`): Identifies the item. The accordion's value lists the open items' values. Defaults to a generated id.
|
|
1265
|
+
|
|
1266
|
+
### AccordionHeader
|
|
1267
|
+
|
|
1268
|
+
The header takes no options of its own, only the props of the element it renders.
|
|
1269
|
+
|
|
1270
|
+
### AccordionTrigger
|
|
1271
|
+
|
|
1272
|
+
- `disabled?` (`boolean`): Stops this trigger from opening or closing the item. It stays focusable.
|
|
1273
|
+
|
|
1274
|
+
### AccordionPanel
|
|
1275
|
+
|
|
1276
|
+
- `keepMounted?` (`boolean`): Whether the panel stays in the page, hidden, while closed. Overrides the accordion's.
|
|
1277
|
+
- `hiddenUntilFound?` (`boolean`): Whether the panel stays findable with `hidden="until-found"` while closed. Overrides the accordion's.
|
|
1278
|
+
### State
|
|
1279
|
+
|
|
1280
|
+
Read the full state from the element's `api`. Part of it also shows as data attributes: the item, header, trigger and panel write `data-open` or `data-closed` and `data-disabled`, the item adds `data-index`, and the root writes only `data-disabled`. [Styling](#styling) lists every attribute.
|
|
1281
|
+
|
|
1282
|
+
`Accordion`:
|
|
1283
|
+
|
|
1284
|
+
- `value` (`T[]`): The open items' values.
|
|
1285
|
+
- `disabled` (`boolean`): Whether the whole accordion is disabled, by its `disabled` option or a disabled fieldset around it.
|
|
1286
|
+
- `multiple` (`boolean`): Whether several items can be open at once.
|
|
1287
|
+
|
|
1288
|
+
`AccordionItem`, `AccordionHeader`, `AccordionTrigger` and `AccordionPanel`:
|
|
1289
|
+
|
|
1290
|
+
- `open` (`boolean`): Whether the accordion's `value` holds this item's value.
|
|
1291
|
+
- `disabled` (`boolean`): Whether the item can't open or close.
|
|
1292
|
+
- `index` (`number`): The item's position among the accordion's items, from 0. -1 until it's in the page.
|
|
1293
|
+
|
|
1294
|
+
### Reasons
|
|
1295
|
+
|
|
1296
|
+
`event.detail.reason` says why items opened or closed, for `valuechange` and every item's `openchange`.
|
|
1297
|
+
|
|
1298
|
+
- `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`.
|
|
1299
|
+
- `find-in-page` (`Event`): The browser's find in page matched hidden content.
|
|
1300
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
1301
|
+
|
|
1302
|
+
### Controller
|
|
1303
|
+
|
|
1304
|
+
The element's `api` property has the same methods.
|
|
1305
|
+
|
|
1306
|
+
Every write method takes an optional `{ reason, event }`, fires the item and accordion events like a press, and returns `false` when the change is canceled or does nothing.
|
|
1307
|
+
|
|
1308
|
+
- `getValue()` (`() => T[]`): The open items' values.
|
|
1309
|
+
- `setValue(value, options?)` (`(value: T[], options?: AccordionChangeOptions | undefined) => boolean`): Sets the open items. Only the first counts unless `multiple` is set. Returns false when canceled.
|
|
1310
|
+
- `isOpen(value)` (`(value: T) => boolean`): Whether an item is open.
|
|
1311
|
+
- `open(value, options?)` (`(value: T, options?: AccordionChangeOptions | undefined) => boolean`): Opens an item. With one item open at a time, closes the others.
|
|
1312
|
+
- `close(value, options?)` (`(value: T, options?: AccordionChangeOptions | undefined) => boolean`): Closes an item, unless it's the one item that must stay open.
|
|
1313
|
+
- `toggle(value, options?)` (`(value: T, options?: AccordionChangeOptions | undefined) => boolean`): Opens a closed item, or closes an open one.
|
|
1314
|
+
- `openAll(options?)` (`(options?: AccordionChangeOptions | undefined) => boolean`): Opens every enabled item. Only when `multiple` is set.
|
|
1315
|
+
- `closeAll(options?)` (`(options?: AccordionChangeOptions | undefined) => boolean`): Closes every item that may close.
|
|
1316
|
+
- `focus(value?)` (`(value?: T | undefined) => void`): Moves focus to an item's trigger, or the first trigger.
|
|
1317
|
+
|
|
1318
|
+
### Events
|
|
1319
|
+
|
|
1320
|
+
Events fire on their own element and don't bubble, with the new state in `event.detail.value` and the details beside it. Calling `preventDefault()` on `valuechange` or `openchange` cancels the change.
|
|
1321
|
+
|
|
1322
|
+
| Member | On | Description |
|
|
1323
|
+
| -------------------------- | ------------------- | -------------------------------------------------------------------------------------------- |
|
|
1324
|
+
| `value` property | `ui-accordion` | The open items, as an array. Setting it, or the `value` attribute, opens them without events |
|
|
1325
|
+
| `api` property | `ui-accordion` | The [controller](#controller) |
|
|
1326
|
+
| `valuechange` event | `ui-accordion` | `detail: { value, reason, event, source }` |
|
|
1327
|
+
| `openchange` event | `ui-accordion-item` | `detail: { value: open, reason, event, source }`, before `valuechange` |
|
|
1328
|
+
| `openchangecomplete` event | `ui-accordion-item` | `detail: { value: open }`, after the panel's animation |
|
|
1329
|
+
|
|
1330
|
+
## Accessibility
|
|
1331
|
+
|
|
1332
|
+
The accordion follows the [WAI-ARIA accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/). Every line below is an automated test.
|
|
1333
|
+
|
|
1334
|
+
**Expanding and collapsing**
|
|
1335
|
+
|
|
1336
|
+
- The default item is expanded and shows its panel.
|
|
1337
|
+
- Clicking a collapsed trigger expands it and collapses the open one.
|
|
1338
|
+
- Clicking an expanded trigger collapses it.
|
|
1339
|
+
- Clicking a disabled trigger does nothing.
|
|
1340
|
+
- A change can be canceled.
|
|
1341
|
+
- Without a default value, every item starts collapsed.
|
|
1342
|
+
- A default value can expand several items.
|
|
1343
|
+
|
|
1344
|
+
**Multiple**
|
|
1345
|
+
|
|
1346
|
+
- Items expand and collapse independently.
|
|
1347
|
+
- Every item can be expanded at once.
|
|
1348
|
+
|
|
1349
|
+
**Disabled**
|
|
1350
|
+
|
|
1351
|
+
- A disabled item's trigger is marked disabled and stays focusable.
|
|
1352
|
+
- A disabled accordion disables every trigger.
|
|
1353
|
+
|
|
1354
|
+
**Keyboard**
|
|
1355
|
+
|
|
1356
|
+
- Tab moves through every trigger in order, disabled ones included.
|
|
1357
|
+
- Enter toggles the focused trigger's panel.
|
|
1358
|
+
- Space toggles the focused trigger's panel.
|
|
1359
|
+
- Enter on a disabled trigger does nothing.
|
|
1360
|
+
- Tab moves from an expanded trigger into its panel.
|
|
1361
|
+
- Keys pressed in quick succession are never lost.
|
|
1362
|
+
- Arrow keys leave focus where it is.
|
|
1363
|
+
|
|
1364
|
+
**Structure**
|
|
1365
|
+
|
|
1366
|
+
- Each trigger sits in a heading.
|
|
1367
|
+
- The trigger controls its panel, and the panel is labelled by its trigger.
|
|
1368
|
+
|
|
1369
|
+
**Mounting**
|
|
1370
|
+
|
|
1371
|
+
- Collapsed panels leave the page by default.
|
|
1372
|
+
- With keepMounted, collapsed panels stay in the page, hidden.
|
|
1373
|
+
- With keepMounted, a class that sets display doesn't show collapsed panels.
|
|
1374
|
+
- With hiddenUntilFound, collapsed panels stay findable.
|
|
1375
|
+
- Finding text in a collapsed panel expands it.
|
|
1376
|
+
- Following a link into a collapsed panel expands it.
|
|
1377
|
+
- The browser's own find expands a collapsed panel.
|
|
1378
|
+
|
|
1379
|
+
Runs in React, Vue and web components.
|
|
1380
|
+
|
|
1381
|
+
Beyond the pattern's baseline:
|
|
1382
|
+
|
|
1383
|
+
**Enhancements**
|
|
1384
|
+
|
|
1385
|
+
- With collapsible off, the open trigger is marked disabled and stays open.
|
|
1386
|
+
- Closing a panel that holds focus moves focus to its trigger.
|
|
1387
|
+
- A canceled find in page keeps the panel hidden until found.
|
|
1388
|
+
- An infinite exit animation still unmounts the panel.
|
|
1389
|
+
- A controller expands and collapses items from anywhere.
|
|
1390
|
+
- An item's onOpenChangeComplete runs once its panel has finished animating.
|
|
1391
|
+
- Find in page opens a panel at once, without an animation.
|
|
1392
|
+
|
|
1393
|
+
Runs in React, Vue and web components.
|
|
1394
|
+
|
|
1395
|
+
Every panel is a region labelled by its trigger, so screen readers can list open panels as landmarks. With many panels open at once, such as `multiple` with Expand all, keep the number of items modest so that list stays short.
|
|
1396
|
+
|
|
1397
|
+
Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:
|
|
1398
|
+
|
|
1399
|
+
- Announcements while using the accordion with a keyboard.
|
|
1400
|
+
|
|
1401
|
+
Runs in React, Vue and web components.
|