@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,1433 @@
|
|
|
1
|
+
# Popover
|
|
2
|
+
|
|
3
|
+
Edit or explore something beside its trigger.
|
|
4
|
+
|
|
5
|
+
A popover keeps a small task close to the thing it belongs to. Put fields, buttons, or other interactive content inside; it handles placement, focus, and dismissal.
|
|
6
|
+
|
|
7
|
+
```ts title="basic.ts"
|
|
8
|
+
import "@ui-sdk/web/field"
|
|
9
|
+
import "@ui-sdk/web/input"
|
|
10
|
+
import "@ui-sdk/web/popover"
|
|
11
|
+
|
|
12
|
+
function mount(root: HTMLElement) {
|
|
13
|
+
root.innerHTML = `
|
|
14
|
+
<ui-popover>
|
|
15
|
+
<ui-popover-trigger>
|
|
16
|
+
<button>Share</button>
|
|
17
|
+
</ui-popover-trigger>
|
|
18
|
+
<ui-popover-content>
|
|
19
|
+
<ui-popover-title>Share this document</ui-popover-title>
|
|
20
|
+
<ui-popover-description>Anyone with the link can view it.</ui-popover-description>
|
|
21
|
+
<ui-field>
|
|
22
|
+
<ui-field-label>Link</ui-field-label>
|
|
23
|
+
<ui-input>
|
|
24
|
+
<input value="acme.app/d/q3-roadmap" readonly />
|
|
25
|
+
</ui-input>
|
|
26
|
+
</ui-field>
|
|
27
|
+
<div data-slot="row">
|
|
28
|
+
<ui-popover-close>
|
|
29
|
+
<button>Cancel</button>
|
|
30
|
+
</ui-popover-close>
|
|
31
|
+
<ui-popover-close>
|
|
32
|
+
<button data-variant="solid">Copy Link</button>
|
|
33
|
+
</ui-popover-close>
|
|
34
|
+
</div>
|
|
35
|
+
</ui-popover-content>
|
|
36
|
+
</ui-popover>
|
|
37
|
+
`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { mount }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Press Share to open the popover, then close it with Cancel, Copy Link, Escape, or a press outside. Focus returns to Share.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install @ui-sdk/web @ui-sdk/core
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Importing the component defines its elements. Import the field too if you use it. On the server the import does nothing.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import "@ui-sdk/web/popover"
|
|
55
|
+
import "@ui-sdk/web/field"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Composition
|
|
59
|
+
|
|
60
|
+
Use the following composition to build a `ui-popover`:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
ui-popover
|
|
64
|
+
├── ui-popover-trigger
|
|
65
|
+
│ └── button
|
|
66
|
+
└── ui-popover-content
|
|
67
|
+
├── ui-popover-arrow
|
|
68
|
+
├── ui-popover-title
|
|
69
|
+
├── ui-popover-description
|
|
70
|
+
└── ui-popover-close
|
|
71
|
+
└── button
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The content has the dialog role. Its title names it and its description adds context. The arrow is optional, and a popover can have several Close parts. The trigger can live inside the popover or [connect from elsewhere](#triggers-outside-the-popover).
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
Import the parts and put them together:
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<script type="module">
|
|
82
|
+
import "@ui-sdk/web/popover"
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<ui-popover>
|
|
86
|
+
<ui-popover-trigger>
|
|
87
|
+
<button>Share</button>
|
|
88
|
+
</ui-popover-trigger>
|
|
89
|
+
<ui-popover-content>
|
|
90
|
+
<ui-popover-title>Share this document</ui-popover-title>
|
|
91
|
+
<ui-popover-description
|
|
92
|
+
>Anyone with the link can view it.</ui-popover-description
|
|
93
|
+
>
|
|
94
|
+
<ui-popover-close>
|
|
95
|
+
<button>Done</button>
|
|
96
|
+
</ui-popover-close>
|
|
97
|
+
</ui-popover-content>
|
|
98
|
+
</ui-popover>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Forms
|
|
102
|
+
|
|
103
|
+
An ordinary form can save a value and then close the popover. Focus starts on the first field and returns to the trigger when the task finishes. A form with `method="dialog"` closes on submission and returns its submit button's value; the [row editor](#triggers-outside-the-popover) shows that version.
|
|
104
|
+
|
|
105
|
+
```ts title="form.ts"
|
|
106
|
+
import "@ui-sdk/web/field"
|
|
107
|
+
import "@ui-sdk/web/input"
|
|
108
|
+
import "@ui-sdk/web/popover"
|
|
109
|
+
import { log } from "./log"
|
|
110
|
+
|
|
111
|
+
function mount(root: HTMLElement) {
|
|
112
|
+
root.innerHTML = `
|
|
113
|
+
<ui-popover>
|
|
114
|
+
<ui-popover-trigger>
|
|
115
|
+
<button>Edit Profile</button>
|
|
116
|
+
</ui-popover-trigger>
|
|
117
|
+
<ui-popover-content>
|
|
118
|
+
<ui-popover-title>Edit profile</ui-popover-title>
|
|
119
|
+
<ui-popover-description>Your name shows on your posts.</ui-popover-description>
|
|
120
|
+
<form data-slot="stack">
|
|
121
|
+
<ui-field>
|
|
122
|
+
<ui-field-label>Name</ui-field-label>
|
|
123
|
+
<ui-input>
|
|
124
|
+
<input name="name" required value="Hollis Grant" />
|
|
125
|
+
</ui-input>
|
|
126
|
+
</ui-field>
|
|
127
|
+
<div data-slot="row">
|
|
128
|
+
<ui-popover-close>
|
|
129
|
+
<button type="button">Cancel</button>
|
|
130
|
+
</ui-popover-close>
|
|
131
|
+
<button type="submit" data-variant="solid">Save</button>
|
|
132
|
+
</div>
|
|
133
|
+
</form>
|
|
134
|
+
</ui-popover-content>
|
|
135
|
+
</ui-popover>
|
|
136
|
+
`
|
|
137
|
+
const popover = root.querySelector("ui-popover")!
|
|
138
|
+
root.querySelector("form")!.addEventListener("submit", (event) => {
|
|
139
|
+
event.preventDefault()
|
|
140
|
+
log(
|
|
141
|
+
root,
|
|
142
|
+
`Saved ${new FormData(event.target as HTMLFormElement).get("name")}`
|
|
143
|
+
)
|
|
144
|
+
popover.open = false
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export { mount }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Open Edit Profile, change the name, and press Save. The popover closes and the console logs the saved name.
|
|
152
|
+
|
|
153
|
+
## Keep It Open While Editing
|
|
154
|
+
|
|
155
|
+
Set `dismissible` to false while there is unsaved work. Escape and outside presses then keep the popover open and fire `dismissattempt`, so you can ask whether to discard it. Focus leaving is refused quietly. The trigger, Close parts, and controller can still close it.
|
|
156
|
+
|
|
157
|
+
```ts title="draft.ts"
|
|
158
|
+
import "@ui-sdk/web/field"
|
|
159
|
+
import "@ui-sdk/web/popover"
|
|
160
|
+
import { log } from "./log"
|
|
161
|
+
|
|
162
|
+
function mount(root: HTMLElement) {
|
|
163
|
+
root.innerHTML = `
|
|
164
|
+
<ui-popover>
|
|
165
|
+
<ui-popover-trigger>
|
|
166
|
+
<button>Add Note</button>
|
|
167
|
+
</ui-popover-trigger>
|
|
168
|
+
<ui-popover-content>
|
|
169
|
+
<ui-popover-title>Add a note</ui-popover-title>
|
|
170
|
+
<ui-popover-description>Only your team can see it.</ui-popover-description>
|
|
171
|
+
<ui-field>
|
|
172
|
+
<ui-field-label>Note</ui-field-label>
|
|
173
|
+
<textarea placeholder="Call the printer about the proofs."></textarea>
|
|
174
|
+
</ui-field>
|
|
175
|
+
<div data-slot="row" role="alert" hidden>
|
|
176
|
+
Discard your note?
|
|
177
|
+
<div data-slot="row">
|
|
178
|
+
<button type="button" data-size="sm">Keep Editing</button>
|
|
179
|
+
<button type="button" data-size="sm" data-variant="solid">Discard</button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div data-slot="row">
|
|
183
|
+
<ui-popover-close>
|
|
184
|
+
<button>Cancel</button>
|
|
185
|
+
</ui-popover-close>
|
|
186
|
+
<button type="button" data-variant="solid">Save</button>
|
|
187
|
+
</div>
|
|
188
|
+
</ui-popover-content>
|
|
189
|
+
</ui-popover>
|
|
190
|
+
`
|
|
191
|
+
const popover = root.querySelector("ui-popover")!
|
|
192
|
+
const note = root.querySelector("textarea")!
|
|
193
|
+
const alert = root.querySelector<HTMLElement>('[role="alert"]')!
|
|
194
|
+
const actions = root.querySelector<HTMLElement>(
|
|
195
|
+
'ui-popover-content > [data-slot="row"]:not([role])'
|
|
196
|
+
)!
|
|
197
|
+
const [keep, discard] = alert.querySelectorAll("button")
|
|
198
|
+
const ask = (asking: boolean) => {
|
|
199
|
+
alert.hidden = !asking
|
|
200
|
+
actions.hidden = asking
|
|
201
|
+
}
|
|
202
|
+
const reset = () => {
|
|
203
|
+
ask(false)
|
|
204
|
+
note.value = ""
|
|
205
|
+
popover.removeAttribute("dismissible")
|
|
206
|
+
}
|
|
207
|
+
const close = () => {
|
|
208
|
+
reset()
|
|
209
|
+
popover.open = false
|
|
210
|
+
}
|
|
211
|
+
note.addEventListener("input", () => {
|
|
212
|
+
if (note.value) {
|
|
213
|
+
popover.setAttribute("dismissible", "false")
|
|
214
|
+
} else {
|
|
215
|
+
popover.removeAttribute("dismissible")
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
popover.addEventListener("dismissattempt", (event) => {
|
|
219
|
+
log(root, `Kept open (${event.detail.reason})`)
|
|
220
|
+
ask(true)
|
|
221
|
+
})
|
|
222
|
+
keep.addEventListener("click", () => {
|
|
223
|
+
ask(false)
|
|
224
|
+
note.focus()
|
|
225
|
+
})
|
|
226
|
+
discard.addEventListener("click", () => {
|
|
227
|
+
log(root, "Discarded the note")
|
|
228
|
+
close()
|
|
229
|
+
})
|
|
230
|
+
actions
|
|
231
|
+
.querySelector("[data-variant=solid]")!
|
|
232
|
+
.addEventListener("click", () => {
|
|
233
|
+
log(root, note.value ? "Saved the note" : "Nothing to save")
|
|
234
|
+
close()
|
|
235
|
+
})
|
|
236
|
+
popover.addEventListener("openchange", (event) => {
|
|
237
|
+
if (!event.detail.value) {
|
|
238
|
+
reset()
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export { mount }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Open Add Note, type something, then press Escape or click outside. Keep Editing returns to the field; Discard closes the popover and clears the note.
|
|
247
|
+
|
|
248
|
+
## Placement
|
|
249
|
+
|
|
250
|
+
`side` chooses an edge of the trigger, `bottom` by default, and `align` lines the popover up with its start, center, or end. Start and end follow text direction. The popover flips or shifts when the requested position doesn't fit, and `data-side` and `data-align` report where it landed. The [popup guide](../popups.md#place-the-content) covers the shared positioning options.
|
|
251
|
+
|
|
252
|
+
```ts title="placement.ts"
|
|
253
|
+
import "@ui-sdk/web/popover"
|
|
254
|
+
import "@ui-sdk/web/select"
|
|
255
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
256
|
+
|
|
257
|
+
const sides = [
|
|
258
|
+
{ value: "top", label: "Top" },
|
|
259
|
+
{ value: "right", label: "Right" },
|
|
260
|
+
{ value: "bottom", label: "Bottom" },
|
|
261
|
+
{ value: "left", label: "Left" },
|
|
262
|
+
]
|
|
263
|
+
const aligns = [
|
|
264
|
+
{ value: "start", label: "Start" },
|
|
265
|
+
{ value: "center", label: "Center" },
|
|
266
|
+
{ value: "end", label: "End" },
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
const check = createElement(Check, { "aria-hidden": "true" }).outerHTML
|
|
270
|
+
const chevron = createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML
|
|
271
|
+
|
|
272
|
+
function select(
|
|
273
|
+
attribute: string,
|
|
274
|
+
label: string,
|
|
275
|
+
value: string,
|
|
276
|
+
options: { value: string; label: string }[]
|
|
277
|
+
) {
|
|
278
|
+
return `
|
|
279
|
+
<ui-select data-attribute="${attribute}" default-value="${value}">
|
|
280
|
+
<ui-select-label>${label}</ui-select-label>
|
|
281
|
+
<ui-select-trigger>
|
|
282
|
+
<button>
|
|
283
|
+
<ui-select-value></ui-select-value>
|
|
284
|
+
${chevron}
|
|
285
|
+
</button>
|
|
286
|
+
</ui-select-trigger>
|
|
287
|
+
<ui-select-content hidden>
|
|
288
|
+
${options
|
|
289
|
+
.map(
|
|
290
|
+
(option) => `
|
|
291
|
+
<ui-select-item value="${option.value}">
|
|
292
|
+
${option.label}
|
|
293
|
+
<ui-select-item-indicator>${check}</ui-select-item-indicator>
|
|
294
|
+
</ui-select-item>
|
|
295
|
+
`
|
|
296
|
+
)
|
|
297
|
+
.join("")}
|
|
298
|
+
</ui-select-content>
|
|
299
|
+
</ui-select>
|
|
300
|
+
`
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function mount(root: HTMLElement) {
|
|
304
|
+
root.innerHTML = `
|
|
305
|
+
<div data-slot="stack">
|
|
306
|
+
<div data-slot="controls">
|
|
307
|
+
${select("side", "Side", "bottom", sides)}
|
|
308
|
+
${select("align", "Align", "center", aligns)}
|
|
309
|
+
</div>
|
|
310
|
+
<ui-popover>
|
|
311
|
+
<ui-popover-trigger>
|
|
312
|
+
<button>Delivery</button>
|
|
313
|
+
</ui-popover-trigger>
|
|
314
|
+
<ui-popover-content side="bottom" align="center" side-offset="8">
|
|
315
|
+
<ui-popover-title>Delivery window</ui-popover-title>
|
|
316
|
+
<ui-popover-description>Arrives Tuesday between 9 AM and noon.</ui-popover-description>
|
|
317
|
+
</ui-popover-content>
|
|
318
|
+
</ui-popover>
|
|
319
|
+
</div>
|
|
320
|
+
`
|
|
321
|
+
const content = root.querySelector("ui-popover-content")!
|
|
322
|
+
for (const control of root.querySelectorAll("ui-select")) {
|
|
323
|
+
control.addEventListener("valuechange", (event) => {
|
|
324
|
+
const value = event.detail.value
|
|
325
|
+
if (typeof value === "string") {
|
|
326
|
+
content.setAttribute(control.dataset.attribute!, value)
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export { mount }
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Choose a side and an alignment, then open Delivery. Near a window edge, the popover flips or shifts to stay visible.
|
|
336
|
+
|
|
337
|
+
## Offsets
|
|
338
|
+
|
|
339
|
+
`side-offset` is the gap to the trigger, 4 pixels by default. `align-offset` shifts the popover along the trigger's edge, and `collision-padding` is the space kept from the edges of the window.
|
|
340
|
+
|
|
341
|
+
```ts title="offsets.ts"
|
|
342
|
+
import "@ui-sdk/web/popover"
|
|
343
|
+
import "@ui-sdk/web/select"
|
|
344
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
345
|
+
|
|
346
|
+
const choices = [
|
|
347
|
+
{
|
|
348
|
+
attribute: "side-offset",
|
|
349
|
+
label: "Side offset",
|
|
350
|
+
value: "8",
|
|
351
|
+
values: ["4", "8", "16", "24"],
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
attribute: "align-offset",
|
|
355
|
+
label: "Align offset",
|
|
356
|
+
value: "0",
|
|
357
|
+
values: ["-16", "0", "16"],
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
attribute: "collision-padding",
|
|
361
|
+
label: "Collision padding",
|
|
362
|
+
value: "8",
|
|
363
|
+
values: ["8", "24", "48"],
|
|
364
|
+
},
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
const check = createElement(Check, { "aria-hidden": "true" }).outerHTML
|
|
368
|
+
const chevron = createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML
|
|
369
|
+
|
|
370
|
+
function mount(root: HTMLElement) {
|
|
371
|
+
root.innerHTML = `
|
|
372
|
+
<div data-slot="stack">
|
|
373
|
+
<div data-slot="controls">
|
|
374
|
+
${choices
|
|
375
|
+
.map(
|
|
376
|
+
(choice) => `
|
|
377
|
+
<ui-select data-attribute="${choice.attribute}" default-value="${choice.value}">
|
|
378
|
+
<ui-select-label>${choice.label}</ui-select-label>
|
|
379
|
+
<ui-select-trigger>
|
|
380
|
+
<button>
|
|
381
|
+
<ui-select-value></ui-select-value>
|
|
382
|
+
${chevron}
|
|
383
|
+
</button>
|
|
384
|
+
</ui-select-trigger>
|
|
385
|
+
<ui-select-content hidden>
|
|
386
|
+
${choice.values
|
|
387
|
+
.map(
|
|
388
|
+
(value) => `
|
|
389
|
+
<ui-select-item value="${value}">
|
|
390
|
+
${value}px
|
|
391
|
+
<ui-select-item-indicator>${check}</ui-select-item-indicator>
|
|
392
|
+
</ui-select-item>
|
|
393
|
+
`
|
|
394
|
+
)
|
|
395
|
+
.join("")}
|
|
396
|
+
</ui-select-content>
|
|
397
|
+
</ui-select>
|
|
398
|
+
`
|
|
399
|
+
)
|
|
400
|
+
.join("")}
|
|
401
|
+
</div>
|
|
402
|
+
<ui-popover>
|
|
403
|
+
<ui-popover-trigger>
|
|
404
|
+
<button>Delivery</button>
|
|
405
|
+
</ui-popover-trigger>
|
|
406
|
+
<ui-popover-content align="start" side-offset="8" align-offset="0" collision-padding="8">
|
|
407
|
+
<ui-popover-title>Delivery window</ui-popover-title>
|
|
408
|
+
<ui-popover-description>Arrives Tuesday between 9 AM and noon.</ui-popover-description>
|
|
409
|
+
</ui-popover-content>
|
|
410
|
+
</ui-popover>
|
|
411
|
+
</div>
|
|
412
|
+
`
|
|
413
|
+
const content = root.querySelector("ui-popover-content")!
|
|
414
|
+
for (const control of root.querySelectorAll("ui-select")) {
|
|
415
|
+
control.addEventListener("valuechange", (event) => {
|
|
416
|
+
const value = event.detail.value
|
|
417
|
+
if (typeof value === "string") {
|
|
418
|
+
content.setAttribute(control.dataset.attribute!, value)
|
|
419
|
+
}
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export { mount }
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Change the offsets and the padding, then open Delivery. The popover moves away from the trigger and along it.
|
|
428
|
+
|
|
429
|
+
## Arrow
|
|
430
|
+
|
|
431
|
+
`ui-popover-arrow` follows the side the popover actually uses, including after a flip, and gets the same `data-side`. `arrow-padding` keeps it away from the corners; `data-arrow-clamped` tells you when it can't point at the middle of the trigger. This example cancels dismissal when you press its side buttons, so the open arrow can move.
|
|
432
|
+
|
|
433
|
+
```ts title="arrow.ts"
|
|
434
|
+
import "@ui-sdk/web/popover"
|
|
435
|
+
import { CircleQuestionMark, createElement } from "lucide"
|
|
436
|
+
|
|
437
|
+
const sides = [
|
|
438
|
+
{ value: "top", label: "Top" },
|
|
439
|
+
{ value: "right", label: "Right" },
|
|
440
|
+
{ value: "bottom", label: "Bottom" },
|
|
441
|
+
{ value: "left", label: "Left" },
|
|
442
|
+
]
|
|
443
|
+
|
|
444
|
+
function mount(root: HTMLElement) {
|
|
445
|
+
root.innerHTML = `
|
|
446
|
+
<div data-slot="stack">
|
|
447
|
+
<div data-slot="controls" role="group" aria-label="Side">
|
|
448
|
+
${sides
|
|
449
|
+
.map(
|
|
450
|
+
(option) =>
|
|
451
|
+
`<button type="button" data-size="sm" value="${option.value}" aria-pressed="${option.value === "bottom"}">${option.label}</button>`
|
|
452
|
+
)
|
|
453
|
+
.join("")}
|
|
454
|
+
</div>
|
|
455
|
+
<ui-popover>
|
|
456
|
+
<ui-popover-trigger>
|
|
457
|
+
<button aria-label="Help">${createElement(CircleQuestionMark, { "aria-hidden": "true" }).outerHTML}</button>
|
|
458
|
+
</ui-popover-trigger>
|
|
459
|
+
<ui-popover-content side="bottom" side-offset="10" arrow-padding="12">
|
|
460
|
+
<ui-popover-arrow></ui-popover-arrow>
|
|
461
|
+
<ui-popover-title>Keyboard shortcuts</ui-popover-title>
|
|
462
|
+
<ui-popover-description>Press ? anywhere to see them all.</ui-popover-description>
|
|
463
|
+
</ui-popover-content>
|
|
464
|
+
</ui-popover>
|
|
465
|
+
</div>
|
|
466
|
+
`
|
|
467
|
+
const controls = root.querySelector<HTMLElement>("[data-slot=controls]")!
|
|
468
|
+
const content = root.querySelector("ui-popover-content")!
|
|
469
|
+
controls.addEventListener("click", (event) => {
|
|
470
|
+
const button = (event.target as HTMLElement).closest("button")
|
|
471
|
+
if (!button) {
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
content.setAttribute("side", button.value)
|
|
475
|
+
for (const other of controls.querySelectorAll("button")) {
|
|
476
|
+
other.setAttribute("aria-pressed", String(other === button))
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
root.querySelector("ui-popover")!.addEventListener("openchange", (event) => {
|
|
480
|
+
const { reason, event: cause } = event.detail
|
|
481
|
+
const inControls =
|
|
482
|
+
cause?.target instanceof Node && controls.contains(cause.target)
|
|
483
|
+
if (inControls && reason !== "escape-key") {
|
|
484
|
+
event.preventDefault()
|
|
485
|
+
}
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export { mount }
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Open Help, then press Top, Right, Bottom, or Left. The popover stays open while it and its arrow move to that side.
|
|
493
|
+
|
|
494
|
+
Keep overflow visible around an arrow that extends outside the panel. If the body needs to scroll, put scrolling on an inner element. You can also draw an arrow with `::before`, using `data-side` and `--arrow-offset` to position it.
|
|
495
|
+
|
|
496
|
+
## Anchor
|
|
497
|
+
|
|
498
|
+
Set `anchor` to a CSS selector to position against something other than the trigger. The position is read again as the anchor moves.
|
|
499
|
+
|
|
500
|
+
```ts title="anchor.ts"
|
|
501
|
+
import "@ui-sdk/web/checkbox"
|
|
502
|
+
import "@ui-sdk/web/popover"
|
|
503
|
+
import { Check, createElement, SlidersHorizontal } from "lucide"
|
|
504
|
+
|
|
505
|
+
const types = [
|
|
506
|
+
{ label: "Documents", checked: true },
|
|
507
|
+
{ label: "Images", checked: false },
|
|
508
|
+
{ label: "Shared with me", checked: true },
|
|
509
|
+
]
|
|
510
|
+
|
|
511
|
+
const check = createElement(Check, { "aria-hidden": "true" }).outerHTML
|
|
512
|
+
|
|
513
|
+
let count = 0
|
|
514
|
+
|
|
515
|
+
function mount(root: HTMLElement) {
|
|
516
|
+
const id = `popover-anchor-${++count}`
|
|
517
|
+
root.innerHTML = `
|
|
518
|
+
<div data-slot="row">
|
|
519
|
+
<input id="${id}" type="search" placeholder="Search files…" aria-label="Search files" />
|
|
520
|
+
<ui-popover>
|
|
521
|
+
<ui-popover-trigger>
|
|
522
|
+
<button aria-label="Search filters">${createElement(SlidersHorizontal, { "aria-hidden": "true" }).outerHTML}</button>
|
|
523
|
+
</ui-popover-trigger>
|
|
524
|
+
<ui-popover-content anchor="#${id}" align="start" side-offset="8">
|
|
525
|
+
<ui-popover-title>Filters</ui-popover-title>
|
|
526
|
+
<ui-popover-description>Show only these kinds of files.</ui-popover-description>
|
|
527
|
+
<div data-slot="stack">
|
|
528
|
+
${types
|
|
529
|
+
.map(
|
|
530
|
+
(type) => `
|
|
531
|
+
<label data-slot="label">
|
|
532
|
+
<ui-checkbox${type.checked ? " default-checked" : ""}>
|
|
533
|
+
<ui-checkbox-indicator>${check}</ui-checkbox-indicator>
|
|
534
|
+
</ui-checkbox>
|
|
535
|
+
${type.label}
|
|
536
|
+
</label>
|
|
537
|
+
`
|
|
538
|
+
)
|
|
539
|
+
.join("")}
|
|
540
|
+
</div>
|
|
541
|
+
<div data-slot="row">
|
|
542
|
+
<ui-popover-close>
|
|
543
|
+
<button>Cancel</button>
|
|
544
|
+
</ui-popover-close>
|
|
545
|
+
<ui-popover-close>
|
|
546
|
+
<button data-variant="solid">Apply</button>
|
|
547
|
+
</ui-popover-close>
|
|
548
|
+
</div>
|
|
549
|
+
</ui-popover-content>
|
|
550
|
+
</ui-popover>
|
|
551
|
+
</div>
|
|
552
|
+
`
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export { mount }
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Press the filters button. The popover lines up with the search field instead of the small button beside it.
|
|
559
|
+
|
|
560
|
+
## Modal
|
|
561
|
+
|
|
562
|
+
`modal` controls how the popover shares the page:
|
|
563
|
+
|
|
564
|
+
- `"false"`, the default, leaves the page usable, and focus leaving the popover closes it.
|
|
565
|
+
- `"trap-focus"` keeps Tab inside while the page can still scroll and receive clicks.
|
|
566
|
+
- `"true"` also makes the rest of the page inert and locks scrolling.
|
|
567
|
+
|
|
568
|
+
```ts title="modal.ts"
|
|
569
|
+
import "@ui-sdk/web/field"
|
|
570
|
+
import "@ui-sdk/web/textarea"
|
|
571
|
+
import "@ui-sdk/web/popover"
|
|
572
|
+
import "@ui-sdk/web/select"
|
|
573
|
+
import { Check, ChevronDown, createElement } from "lucide"
|
|
574
|
+
|
|
575
|
+
const modes = [
|
|
576
|
+
{ value: "false", label: "Non-modal" },
|
|
577
|
+
{ value: "trap-focus", label: "Trap focus" },
|
|
578
|
+
{ value: "true", label: "Modal" },
|
|
579
|
+
]
|
|
580
|
+
|
|
581
|
+
type Mode = "false" | "trap-focus" | "true"
|
|
582
|
+
|
|
583
|
+
const notes = {
|
|
584
|
+
false: "Tab can leave, and focus leaving closes it.",
|
|
585
|
+
"trap-focus": "Tab stays inside. The page still scrolls and takes clicks.",
|
|
586
|
+
true: "Tab stays inside. The page is inert and doesn't scroll.",
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const check = createElement(Check, { "aria-hidden": "true" }).outerHTML
|
|
590
|
+
|
|
591
|
+
function mount(root: HTMLElement) {
|
|
592
|
+
root.innerHTML = `
|
|
593
|
+
<div data-slot="stack">
|
|
594
|
+
<div data-slot="controls">
|
|
595
|
+
<ui-select default-value="false">
|
|
596
|
+
<ui-select-label>Modal</ui-select-label>
|
|
597
|
+
<ui-select-trigger>
|
|
598
|
+
<button>
|
|
599
|
+
<ui-select-value></ui-select-value>
|
|
600
|
+
${createElement(ChevronDown, { "aria-hidden": "true" }).outerHTML}
|
|
601
|
+
</button>
|
|
602
|
+
</ui-select-trigger>
|
|
603
|
+
<ui-select-content hidden>
|
|
604
|
+
${modes
|
|
605
|
+
.map(
|
|
606
|
+
(option) => `
|
|
607
|
+
<ui-select-item value="${option.value}">
|
|
608
|
+
${option.label}
|
|
609
|
+
<ui-select-item-indicator>${check}</ui-select-item-indicator>
|
|
610
|
+
</ui-select-item>
|
|
611
|
+
`
|
|
612
|
+
)
|
|
613
|
+
.join("")}
|
|
614
|
+
</ui-select-content>
|
|
615
|
+
</ui-select>
|
|
616
|
+
</div>
|
|
617
|
+
<ui-popover modal="false">
|
|
618
|
+
<ui-popover-trigger>
|
|
619
|
+
<button>Add Comment</button>
|
|
620
|
+
</ui-popover-trigger>
|
|
621
|
+
<ui-popover-content>
|
|
622
|
+
<ui-popover-title>Add a comment</ui-popover-title>
|
|
623
|
+
<ui-popover-description>${notes.false}</ui-popover-description>
|
|
624
|
+
<ui-field>
|
|
625
|
+
<ui-field-label>Comment</ui-field-label>
|
|
626
|
+
<ui-textarea>
|
|
627
|
+
<textarea>Looks good to me.</textarea>
|
|
628
|
+
</ui-textarea>
|
|
629
|
+
</ui-field>
|
|
630
|
+
<div data-slot="row">
|
|
631
|
+
<ui-popover-close>
|
|
632
|
+
<button>Cancel</button>
|
|
633
|
+
</ui-popover-close>
|
|
634
|
+
<ui-popover-close>
|
|
635
|
+
<button data-variant="solid">Comment</button>
|
|
636
|
+
</ui-popover-close>
|
|
637
|
+
</div>
|
|
638
|
+
</ui-popover-content>
|
|
639
|
+
</ui-popover>
|
|
640
|
+
</div>
|
|
641
|
+
`
|
|
642
|
+
const popover = root.querySelector("ui-popover")!
|
|
643
|
+
const description = root.querySelector("ui-popover-description")!
|
|
644
|
+
root.querySelector("ui-select")!.addEventListener("valuechange", (event) => {
|
|
645
|
+
const mode = event.detail.value as Mode | null
|
|
646
|
+
if (!mode) {
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
popover.setAttribute("modal", mode)
|
|
650
|
+
description.textContent = notes[mode]
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export { mount }
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Choose a mode, open Add Comment, and try Tab or clicking the page.
|
|
658
|
+
|
|
659
|
+
## Focus
|
|
660
|
+
|
|
661
|
+
Use `initial-focus` to choose the first focused element, or `"none"` to leave focus where it is. `final-focus` controls where focus returns. Opening on hover leaves focus alone; opening with touch focuses the panel itself so no on-screen keyboard appears. See [popup focus](../popups.md#focus) for the shared focus rules.
|
|
662
|
+
|
|
663
|
+
```ts title="focus.ts"
|
|
664
|
+
import "@ui-sdk/web/field"
|
|
665
|
+
import "@ui-sdk/web/input"
|
|
666
|
+
import "@ui-sdk/web/popover"
|
|
667
|
+
|
|
668
|
+
function mount(root: HTMLElement) {
|
|
669
|
+
root.innerHTML = `
|
|
670
|
+
<div data-slot="row">
|
|
671
|
+
<ui-popover>
|
|
672
|
+
<ui-popover-trigger>
|
|
673
|
+
<button>Rename</button>
|
|
674
|
+
</ui-popover-trigger>
|
|
675
|
+
<ui-popover-content>
|
|
676
|
+
<ui-popover-title>Rename file</ui-popover-title>
|
|
677
|
+
<ui-popover-description>Focus starts in the first field.</ui-popover-description>
|
|
678
|
+
<ui-field>
|
|
679
|
+
<ui-field-label>Name</ui-field-label>
|
|
680
|
+
<ui-input>
|
|
681
|
+
<input value="Q3 roadmap.pdf" />
|
|
682
|
+
</ui-input>
|
|
683
|
+
</ui-field>
|
|
684
|
+
<div data-slot="row">
|
|
685
|
+
<ui-popover-close>
|
|
686
|
+
<button>Cancel</button>
|
|
687
|
+
</ui-popover-close>
|
|
688
|
+
<ui-popover-close>
|
|
689
|
+
<button data-variant="solid">Save</button>
|
|
690
|
+
</ui-popover-close>
|
|
691
|
+
</div>
|
|
692
|
+
</ui-popover-content>
|
|
693
|
+
</ui-popover>
|
|
694
|
+
<ui-popover>
|
|
695
|
+
<ui-popover-trigger>
|
|
696
|
+
<button>Publish</button>
|
|
697
|
+
</ui-popover-trigger>
|
|
698
|
+
<ui-popover-content initial-focus="[data-autofocus]">
|
|
699
|
+
<ui-popover-title>Publish changes?</ui-popover-title>
|
|
700
|
+
<ui-popover-description>Focus starts on Publish, not Cancel.</ui-popover-description>
|
|
701
|
+
<div data-slot="row">
|
|
702
|
+
<ui-popover-close>
|
|
703
|
+
<button>Cancel</button>
|
|
704
|
+
</ui-popover-close>
|
|
705
|
+
<ui-popover-close>
|
|
706
|
+
<button data-autofocus data-variant="solid">Publish</button>
|
|
707
|
+
</ui-popover-close>
|
|
708
|
+
</div>
|
|
709
|
+
</ui-popover-content>
|
|
710
|
+
</ui-popover>
|
|
711
|
+
<ui-popover>
|
|
712
|
+
<ui-popover-trigger>
|
|
713
|
+
<button>Status</button>
|
|
714
|
+
</ui-popover-trigger>
|
|
715
|
+
<ui-popover-content initial-focus="none" final-focus="none">
|
|
716
|
+
<ui-popover-title>All systems normal</ui-popover-title>
|
|
717
|
+
<ui-popover-description>Focus stays on the trigger.</ui-popover-description>
|
|
718
|
+
</ui-popover-content>
|
|
719
|
+
</ui-popover>
|
|
720
|
+
</div>
|
|
721
|
+
`
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
export { mount }
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
Open each one: Rename focuses its field, Publish focuses its Publish button, and Status leaves focus on the trigger.
|
|
728
|
+
|
|
729
|
+
## Open on Hover
|
|
730
|
+
|
|
731
|
+
`open-on-hover` adds pointer opening while keeping click, keyboard, and touch activation. `delay` and `close-delay` control the pause before opening and closing. You can cross the gap into the content and interact with it.
|
|
732
|
+
|
|
733
|
+
```ts title="hover.ts"
|
|
734
|
+
import "@ui-sdk/web/popover"
|
|
735
|
+
import { log } from "./log"
|
|
736
|
+
|
|
737
|
+
function mount(root: HTMLElement) {
|
|
738
|
+
root.innerHTML = `
|
|
739
|
+
<ui-popover open-on-hover delay="200" close-delay="150">
|
|
740
|
+
<ui-popover-trigger>
|
|
741
|
+
<button>@kofi</button>
|
|
742
|
+
</ui-popover-trigger>
|
|
743
|
+
<ui-popover-content side-offset="8">
|
|
744
|
+
<ui-popover-arrow></ui-popover-arrow>
|
|
745
|
+
<ui-popover-title>Kofi Mensah</ui-popover-title>
|
|
746
|
+
<ui-popover-description>Engineering manager, platform team.</ui-popover-description>
|
|
747
|
+
<div data-slot="row">
|
|
748
|
+
<button type="button" data-size="sm" data-variant="solid">Follow</button>
|
|
749
|
+
</div>
|
|
750
|
+
</ui-popover-content>
|
|
751
|
+
</ui-popover>
|
|
752
|
+
`
|
|
753
|
+
root
|
|
754
|
+
.querySelector("ui-popover-content button")!
|
|
755
|
+
.addEventListener("click", () => log(root, "Followed @kofi"))
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export { mount }
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Hover @kofi, then move into the popover and press Follow. You can also open it by pressing the trigger.
|
|
762
|
+
|
|
763
|
+
## Controlled
|
|
764
|
+
|
|
765
|
+
Set the element's `open` property and listen for `openchange` to keep another control in sync. Cancel a proposed change with `event.preventDefault()`. See [state and events](../state-and-events.md) for the common change contract.
|
|
766
|
+
|
|
767
|
+
```ts title="controlled.ts"
|
|
768
|
+
import "@ui-sdk/web/checkbox"
|
|
769
|
+
import "@ui-sdk/web/field"
|
|
770
|
+
import "@ui-sdk/web/input"
|
|
771
|
+
import "@ui-sdk/web/popover"
|
|
772
|
+
import { Check, createElement } from "lucide"
|
|
773
|
+
import { log } from "./log"
|
|
774
|
+
|
|
775
|
+
function mount(root: HTMLElement) {
|
|
776
|
+
root.innerHTML = `
|
|
777
|
+
<div data-slot="stack">
|
|
778
|
+
<div data-slot="controls">
|
|
779
|
+
<label data-slot="label">
|
|
780
|
+
<ui-checkbox>
|
|
781
|
+
<ui-checkbox-indicator>${createElement(Check, { "aria-hidden": "true" }).outerHTML}</ui-checkbox-indicator>
|
|
782
|
+
</ui-checkbox>
|
|
783
|
+
Open
|
|
784
|
+
</label>
|
|
785
|
+
</div>
|
|
786
|
+
<ui-popover>
|
|
787
|
+
<ui-popover-trigger>
|
|
788
|
+
<button>Set Status</button>
|
|
789
|
+
</ui-popover-trigger>
|
|
790
|
+
<ui-popover-content>
|
|
791
|
+
<ui-popover-title>Set a status</ui-popover-title>
|
|
792
|
+
<ui-popover-description>Your team sees it next to your name.</ui-popover-description>
|
|
793
|
+
<ui-field>
|
|
794
|
+
<ui-field-label>Status</ui-field-label>
|
|
795
|
+
<ui-input>
|
|
796
|
+
<input value="In a meeting" />
|
|
797
|
+
</ui-input>
|
|
798
|
+
</ui-field>
|
|
799
|
+
<div data-slot="row">
|
|
800
|
+
<ui-popover-close>
|
|
801
|
+
<button>Cancel</button>
|
|
802
|
+
</ui-popover-close>
|
|
803
|
+
<ui-popover-close>
|
|
804
|
+
<button data-variant="solid">Save</button>
|
|
805
|
+
</ui-popover-close>
|
|
806
|
+
</div>
|
|
807
|
+
</ui-popover-content>
|
|
808
|
+
</ui-popover>
|
|
809
|
+
</div>
|
|
810
|
+
`
|
|
811
|
+
const controls = root.querySelector('[data-slot="controls"]')!
|
|
812
|
+
const toggle = root.querySelector("ui-checkbox")!
|
|
813
|
+
const popover = root.querySelector("ui-popover")!
|
|
814
|
+
toggle.addEventListener("checkedchange", (event) => {
|
|
815
|
+
popover.open = event.detail.value === true
|
|
816
|
+
})
|
|
817
|
+
popover.addEventListener("openchange", (event) => {
|
|
818
|
+
const { value, reason, event: cause } = event.detail
|
|
819
|
+
const inControls =
|
|
820
|
+
cause?.target instanceof Node && controls.contains(cause.target)
|
|
821
|
+
if (inControls && reason !== "escape-key") {
|
|
822
|
+
return event.preventDefault()
|
|
823
|
+
}
|
|
824
|
+
toggle.checked = value
|
|
825
|
+
log(root, `${value ? "Opened" : "Closed"} (${reason})`)
|
|
826
|
+
})
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export { mount }
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
Check Open or press Set Status, then close the popover. The console logs each change and its reason.
|
|
833
|
+
|
|
834
|
+
## Keyboard Shortcut
|
|
835
|
+
|
|
836
|
+
Set a trigger's `shortcut` to toggle the popover from the keyboard. `mod+i` means Command+I on Apple platforms and Control+I elsewhere. The Shortcut part displays the platform's keys, and the trigger advertises them through `aria-keyshortcuts`.
|
|
837
|
+
|
|
838
|
+
```ts title="shortcut.ts"
|
|
839
|
+
import "@ui-sdk/web/field"
|
|
840
|
+
import "@ui-sdk/web/input"
|
|
841
|
+
import "@ui-sdk/web/popover"
|
|
842
|
+
|
|
843
|
+
function mount(root: HTMLElement) {
|
|
844
|
+
root.innerHTML = `
|
|
845
|
+
<ui-popover>
|
|
846
|
+
<ui-popover-trigger shortcut="mod+i">
|
|
847
|
+
<button>
|
|
848
|
+
Invite
|
|
849
|
+
<ui-popover-shortcut></ui-popover-shortcut>
|
|
850
|
+
</button>
|
|
851
|
+
</ui-popover-trigger>
|
|
852
|
+
<ui-popover-content>
|
|
853
|
+
<ui-popover-title>Invite people</ui-popover-title>
|
|
854
|
+
<ui-popover-description>They'll get an email with a link to join.</ui-popover-description>
|
|
855
|
+
<ui-field>
|
|
856
|
+
<ui-field-label>Email</ui-field-label>
|
|
857
|
+
<ui-input>
|
|
858
|
+
<input type="email" placeholder="wren@example.com" />
|
|
859
|
+
</ui-input>
|
|
860
|
+
</ui-field>
|
|
861
|
+
<div data-slot="row">
|
|
862
|
+
<ui-popover-close>
|
|
863
|
+
<button>Cancel</button>
|
|
864
|
+
</ui-popover-close>
|
|
865
|
+
<ui-popover-close>
|
|
866
|
+
<button data-variant="solid">Send Invite</button>
|
|
867
|
+
</ui-popover-close>
|
|
868
|
+
</div>
|
|
869
|
+
</ui-popover-content>
|
|
870
|
+
</ui-popover>
|
|
871
|
+
`
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export { mount }
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
Press Command+I or Control+I to open Invite. Press the same shortcut again, or Escape, to close it.
|
|
878
|
+
|
|
879
|
+
## Triggers Outside the Popover
|
|
880
|
+
|
|
881
|
+
Give the popover an `id`, and point each trigger at it with `for`. Each trigger passes its row as its `payload` property; read the active row with `api.getPayload()` or the `payloadchange` event. Only the active trigger is expanded and receives focus back. Opening from another row updates the same panel, so one editor can serve the whole list.
|
|
882
|
+
|
|
883
|
+
`api.open({ payload, anchor })` opens it from code and returns a promise with the closing reason and any `returnValue`. The payload stays available during the exit animation. See [controllers](../controllers.md) for the shared pattern.
|
|
884
|
+
|
|
885
|
+
```ts title="payload.ts"
|
|
886
|
+
import "@ui-sdk/web/field"
|
|
887
|
+
import type { UiPopoverElement } from "@ui-sdk/web/popover"
|
|
888
|
+
import "@ui-sdk/web/popover"
|
|
889
|
+
import { log } from "./log"
|
|
890
|
+
|
|
891
|
+
type Member = { name: string; role: string }
|
|
892
|
+
|
|
893
|
+
const roles = ["Admin", "Editor", "Viewer"]
|
|
894
|
+
|
|
895
|
+
let count = 0
|
|
896
|
+
|
|
897
|
+
function mount(root: HTMLElement) {
|
|
898
|
+
const id = `member-editor-${++count}`
|
|
899
|
+
const members: Member[] = [
|
|
900
|
+
{ name: "Hollis Grant", role: "Owner" },
|
|
901
|
+
{ name: "Inès Moreau", role: "Editor" },
|
|
902
|
+
{ name: "Kofi Mensah", role: "Viewer" },
|
|
903
|
+
{ name: "Wren Takahashi", role: "Viewer" },
|
|
904
|
+
]
|
|
905
|
+
root.innerHTML = `
|
|
906
|
+
<div data-slot="stack">
|
|
907
|
+
<div data-slot="controls">
|
|
908
|
+
<button type="button">Edit Newest Member</button>
|
|
909
|
+
</div>
|
|
910
|
+
<ul data-slot="list"></ul>
|
|
911
|
+
<ui-popover id="${id}">
|
|
912
|
+
<ui-popover-content align="end" side-offset="8">
|
|
913
|
+
<ui-popover-title></ui-popover-title>
|
|
914
|
+
<ui-popover-description>Roles decide what they can change.</ui-popover-description>
|
|
915
|
+
<form data-slot="stack" method="dialog">
|
|
916
|
+
<ui-field>
|
|
917
|
+
<ui-field-label>Role</ui-field-label>
|
|
918
|
+
<select name="role">
|
|
919
|
+
${roles.map((role) => `<option>${role}</option>`).join("")}
|
|
920
|
+
</select>
|
|
921
|
+
</ui-field>
|
|
922
|
+
<div data-slot="row">
|
|
923
|
+
<button type="submit" value="cancel" formnovalidate>Cancel</button>
|
|
924
|
+
<button type="submit" value="save" data-variant="solid">Save</button>
|
|
925
|
+
</div>
|
|
926
|
+
</form>
|
|
927
|
+
</ui-popover-content>
|
|
928
|
+
</ui-popover>
|
|
929
|
+
</div>
|
|
930
|
+
`
|
|
931
|
+
const popover = root.querySelector<UiPopoverElement<Member>>("ui-popover")!
|
|
932
|
+
const form = root.querySelector("form")!
|
|
933
|
+
const list = root.querySelector("ul")!
|
|
934
|
+
|
|
935
|
+
const rows = new Map<Member, HTMLElement>()
|
|
936
|
+
for (const member of members) {
|
|
937
|
+
const item = document.createElement("li")
|
|
938
|
+
item.dataset.slot = "row"
|
|
939
|
+
item.innerHTML = `${member.name}<span data-slot="hint">${member.role}</span>`
|
|
940
|
+
const trigger = Object.assign(
|
|
941
|
+
document.createElement("ui-popover-trigger"),
|
|
942
|
+
{ payload: member }
|
|
943
|
+
)
|
|
944
|
+
trigger.setAttribute("for", id)
|
|
945
|
+
trigger.toggleAttribute("disabled", member.role === "Owner")
|
|
946
|
+
trigger.innerHTML = `<button aria-label="Edit ${member.name}" data-size="sm" data-variant="ghost">Edit</button>`
|
|
947
|
+
item.append(trigger)
|
|
948
|
+
rows.set(member, item)
|
|
949
|
+
list.append(item)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const show = (member?: Member) => {
|
|
953
|
+
root.querySelector("ui-popover-title")!.textContent = member?.name ?? ""
|
|
954
|
+
form.querySelector("select")!.value = member?.role ?? ""
|
|
955
|
+
}
|
|
956
|
+
popover.addEventListener("payloadchange", (event) => {
|
|
957
|
+
const member = event.detail.value
|
|
958
|
+
show(member)
|
|
959
|
+
if (member) {
|
|
960
|
+
log(root, `Editing ${member.name}`)
|
|
961
|
+
}
|
|
962
|
+
})
|
|
963
|
+
popover.addEventListener("openchange", (event) => {
|
|
964
|
+
if (event.detail.value) {
|
|
965
|
+
show(popover.api.getPayload())
|
|
966
|
+
}
|
|
967
|
+
})
|
|
968
|
+
form.addEventListener("submit", (event) => {
|
|
969
|
+
const member = popover.api.getPayload()
|
|
970
|
+
if ((event.submitter as HTMLButtonElement).value !== "save" || !member) {
|
|
971
|
+
return
|
|
972
|
+
}
|
|
973
|
+
member.role = String(new FormData(form).get("role"))
|
|
974
|
+
rows.get(member)!.querySelector("span")!.textContent = member.role
|
|
975
|
+
})
|
|
976
|
+
|
|
977
|
+
const opener = root.querySelector<HTMLButtonElement>(
|
|
978
|
+
"[data-slot=controls] button"
|
|
979
|
+
)!
|
|
980
|
+
opener.addEventListener("click", async () => {
|
|
981
|
+
const result = await popover.api.open({
|
|
982
|
+
payload: members[members.length - 1],
|
|
983
|
+
anchor: opener,
|
|
984
|
+
})
|
|
985
|
+
log(
|
|
986
|
+
root,
|
|
987
|
+
`Closed (${result.reason}), returned ${result.returnValue ?? "nothing"}`
|
|
988
|
+
)
|
|
989
|
+
})
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
export { mount }
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
Edit Inès Moreau or Kofi Mensah and save a new role. The owner can't be edited. Edit Newest Member opens the same editor from code, and the console logs how it closed.
|
|
996
|
+
|
|
997
|
+
## Animation
|
|
998
|
+
|
|
999
|
+
The content has `data-starting-style` while it opens and `data-ending-style` while it closes. Transition from those states, and the popover stays mounted until the exit finishes. `openchangecomplete` fires after the transition, and `keep-mounted` keeps the closed content in the page, hidden. See [styling and animation](../styling-and-animation.md) for the common lifecycle.
|
|
1000
|
+
|
|
1001
|
+
```ts title="animation.ts"
|
|
1002
|
+
import "@ui-sdk/web/popover"
|
|
1003
|
+
import { log } from "./log"
|
|
1004
|
+
|
|
1005
|
+
function mount(root: HTMLElement) {
|
|
1006
|
+
root.innerHTML = `
|
|
1007
|
+
<ui-popover>
|
|
1008
|
+
<ui-popover-trigger>
|
|
1009
|
+
<button>What's New</button>
|
|
1010
|
+
</ui-popover-trigger>
|
|
1011
|
+
<ui-popover-content keep-mounted>
|
|
1012
|
+
<ui-popover-title>What's new</ui-popover-title>
|
|
1013
|
+
<ui-popover-description>Comments now support threads and reactions.</ui-popover-description>
|
|
1014
|
+
<div data-slot="row">
|
|
1015
|
+
<ui-popover-close>
|
|
1016
|
+
<button data-variant="solid">Got It</button>
|
|
1017
|
+
</ui-popover-close>
|
|
1018
|
+
</div>
|
|
1019
|
+
</ui-popover-content>
|
|
1020
|
+
</ui-popover>
|
|
1021
|
+
`
|
|
1022
|
+
const popover = root.querySelector("ui-popover")!
|
|
1023
|
+
popover.addEventListener("openchange", (event) =>
|
|
1024
|
+
log(root, event.detail.value ? "Opening" : "Closing")
|
|
1025
|
+
)
|
|
1026
|
+
popover.addEventListener("openchangecomplete", (event) =>
|
|
1027
|
+
log(root, event.detail.value ? "Open" : "Closed")
|
|
1028
|
+
)
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
export { mount }
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
Open What's New, then press Got It. The console logs Opening, Open, Closing, and Closed.
|
|
1035
|
+
|
|
1036
|
+
## Presentation
|
|
1037
|
+
|
|
1038
|
+
`presentation="sheet"` shows the task as a sheet; `"auto"` chooses a sheet on small touch screens each time it opens. A sheet is modal by default and can be swiped down to dismiss. If dismissal is disabled, it springs back and reports the attempt.
|
|
1039
|
+
|
|
1040
|
+
Style `data-presentation` for both layouts. The sheet manages its own position and movement, so leave its translation alone and avoid the floating panel's fixed width. `--sheet-backdrop` sets the dim behind it.
|
|
1041
|
+
|
|
1042
|
+
```ts title="presentation.ts"
|
|
1043
|
+
import "@ui-sdk/web/field"
|
|
1044
|
+
import "@ui-sdk/web/input"
|
|
1045
|
+
import "@ui-sdk/web/popover"
|
|
1046
|
+
|
|
1047
|
+
function mount(root: HTMLElement) {
|
|
1048
|
+
root.innerHTML = `
|
|
1049
|
+
<ui-popover presentation="sheet">
|
|
1050
|
+
<ui-popover-trigger>
|
|
1051
|
+
<button>Add Reminder</button>
|
|
1052
|
+
</ui-popover-trigger>
|
|
1053
|
+
<ui-popover-content>
|
|
1054
|
+
<ui-popover-title>New reminder</ui-popover-title>
|
|
1055
|
+
<ui-popover-description>We'll notify you at the time you pick.</ui-popover-description>
|
|
1056
|
+
<ui-field>
|
|
1057
|
+
<ui-field-label>Remind me to</ui-field-label>
|
|
1058
|
+
<ui-input>
|
|
1059
|
+
<input value="Send the Q3 roadmap" />
|
|
1060
|
+
</ui-input>
|
|
1061
|
+
</ui-field>
|
|
1062
|
+
<div data-slot="row">
|
|
1063
|
+
<ui-popover-close>
|
|
1064
|
+
<button>Cancel</button>
|
|
1065
|
+
</ui-popover-close>
|
|
1066
|
+
<ui-popover-close>
|
|
1067
|
+
<button data-variant="solid">Save</button>
|
|
1068
|
+
</ui-popover-close>
|
|
1069
|
+
</div>
|
|
1070
|
+
</ui-popover-content>
|
|
1071
|
+
</ui-popover>
|
|
1072
|
+
`
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
export { mount }
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
Open Add Reminder as a sheet. Dismiss it with Cancel, Save, Escape, the dimmed area, or a downward swipe on a touch screen.
|
|
1079
|
+
|
|
1080
|
+
## Container
|
|
1081
|
+
|
|
1082
|
+
Popovers render in place by default and use the browser's top layer. `container` moves their DOM to another element while keyboard order still follows the trigger. See [popups](../popups.md#hidden-content-and-containers) for the shared container behavior.
|
|
1083
|
+
|
|
1084
|
+
```html
|
|
1085
|
+
<div id="details-container"></div>
|
|
1086
|
+
<ui-popover container="#details-container">
|
|
1087
|
+
<ui-popover-trigger>
|
|
1088
|
+
<button>Details</button>
|
|
1089
|
+
</ui-popover-trigger>
|
|
1090
|
+
<ui-popover-content>...</ui-popover-content>
|
|
1091
|
+
</ui-popover>
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
## Custom Rendering
|
|
1095
|
+
|
|
1096
|
+
Elements have no render functions. The trigger and Close parts wrap your own `<button>` or link, and `as-child` on the title uses your own heading. Read state from the data attributes every part sets: here the trigger's button holds both labels, and the stylesheet shows one or the other while `data-open` is set.
|
|
1097
|
+
|
|
1098
|
+
```ts title="custom-rendering.ts"
|
|
1099
|
+
import "@ui-sdk/web/popover"
|
|
1100
|
+
|
|
1101
|
+
function mount(root: HTMLElement) {
|
|
1102
|
+
root.innerHTML = `
|
|
1103
|
+
<ui-popover>
|
|
1104
|
+
<ui-popover-trigger shortcut="mod+u">
|
|
1105
|
+
<button>
|
|
1106
|
+
<span data-hide="open">Show Updates</span>
|
|
1107
|
+
<span data-show="open">Hide Updates</span>
|
|
1108
|
+
<ui-popover-shortcut></ui-popover-shortcut>
|
|
1109
|
+
</button>
|
|
1110
|
+
</ui-popover-trigger>
|
|
1111
|
+
<ui-popover-content>
|
|
1112
|
+
<ui-popover-title as-child><h3>Updates</h3></ui-popover-title>
|
|
1113
|
+
<ui-popover-description>Version 2.4 adds threaded comments.</ui-popover-description>
|
|
1114
|
+
<ui-popover-close>
|
|
1115
|
+
<a href="#">Read the Changelog</a>
|
|
1116
|
+
</ui-popover-close>
|
|
1117
|
+
</ui-popover-content>
|
|
1118
|
+
</ui-popover>
|
|
1119
|
+
`
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
export { mount }
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
Open Show Updates. The trigger's label comes from its state, the title renders as an h3, and Close renders as a link.
|
|
1126
|
+
|
|
1127
|
+
## Styling
|
|
1128
|
+
|
|
1129
|
+
Use `data-open` on the trigger to mark it while its popover shows, and `data-side` and `data-align` to style the content and arrow for where they landed. Put `transform-origin: var(--transform-origin)` on the content so a scale animation starts from the trigger.
|
|
1130
|
+
|
|
1131
|
+
Each part has a `data-slot`, such as `popover-content`, and its state attributes are listed under [Data Attributes](#data-attributes).
|
|
1132
|
+
|
|
1133
|
+
### `<ui-popover-content>`
|
|
1134
|
+
- `--anchor-width`: Width of the anchor, usually the trigger. Use it to match widths.
|
|
1135
|
+
- `--anchor-height`: Height of the anchor.
|
|
1136
|
+
- `--available-width`: Space beside the popup, up to the viewport edge. Not inherited either.
|
|
1137
|
+
- `--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.
|
|
1138
|
+
- `--transform-origin`: The point on the anchor the popup opens from. Use it to scale from the right place.
|
|
1139
|
+
- `--arrow-offset`: Where the anchor's center falls along the popup's edge, kept `arrowPadding` from the corners. Position a ::before arrow with it.
|
|
1140
|
+
- `--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.
|
|
1141
|
+
- `--drawer-dim`: While it presents as a sheet: how dim the page behind is, from 0 to 1. The backdrop's opacity follows it.
|
|
1142
|
+
- `--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.
|
|
1143
|
+
- `--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.
|
|
1144
|
+
- `--sheet-backdrop`: Set it yourself: the color the page dims to while it presents as a sheet. Black at 0.2 by default.
|
|
1145
|
+
|
|
1146
|
+
### `<ui-popover-viewport>`
|
|
1147
|
+
- `--keyboard-inset`: While it presents as a sheet: how many pixels of the page the on-screen keyboard covers, 0 without one.
|
|
1148
|
+
- `--visual-viewport-width`: While it presents as a sheet: the width of the part of the page you can see.
|
|
1149
|
+
- `--visual-viewport-height`: While it presents as a sheet: the height of the part of the page you can see, above the keyboard.
|
|
1150
|
+
- `--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.
|
|
1151
|
+
|
|
1152
|
+
## Data Attributes
|
|
1153
|
+
|
|
1154
|
+
`<ui-popover-title>`, `<ui-popover-description>` and `<ui-popover-close>` have no state attributes.
|
|
1155
|
+
|
|
1156
|
+
### `<ui-popover-trigger>`
|
|
1157
|
+
`[data-slot="popover-trigger"]`
|
|
1158
|
+
|
|
1159
|
+
- `data-closed`: Present while the popover is closed, including while it animates out.
|
|
1160
|
+
- `data-disabled`: Present when the trigger is disabled.
|
|
1161
|
+
- `data-open`: Present while the popover is open. On a trigger, while it's open from that trigger.
|
|
1162
|
+
|
|
1163
|
+
### `<ui-popover-content>`
|
|
1164
|
+
`[data-slot="popover-content"]`
|
|
1165
|
+
|
|
1166
|
+
- `data-closed`: Present while the popover is closed, including while it animates out.
|
|
1167
|
+
- `data-presentation` (`popover`, `sheet`): How the popover presents this time it's open.
|
|
1168
|
+
- `data-open`: Present while the popover is open. On a trigger, while it's open from that trigger.
|
|
1169
|
+
- `data-starting-style`: Present for the first frame. Style the state you animate from.
|
|
1170
|
+
- `data-ending-style`: Present while the popover animates out. Style the state you animate to.
|
|
1171
|
+
|
|
1172
|
+
### `<ui-popover-arrow>`
|
|
1173
|
+
`[data-slot="popover-arrow"]`
|
|
1174
|
+
|
|
1175
|
+
- `data-closed`: Present while the popover is closed, including while it animates out.
|
|
1176
|
+
- `data-open`: Present while the popover is open. On a trigger, while it's open from that trigger.
|
|
1177
|
+
|
|
1178
|
+
## API Reference
|
|
1179
|
+
|
|
1180
|
+
Props are attributes in kebab case, like `open-on-hover` and `side-offset`: `"false"` is false, and numbers
|
|
1181
|
+
are numbers. `anchor` and `container` take CSS selectors. Callbacks are DOM events.
|
|
1182
|
+
|
|
1183
|
+
### Popover
|
|
1184
|
+
|
|
1185
|
+
- `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.
|
|
1186
|
+
- `open?` (`boolean`): Whether the popover is open. Pass it to control the popover.
|
|
1187
|
+
- `defaultOpen?` (`boolean`): Whether the popover starts open, when uncontrolled.
|
|
1188
|
+
- `openchange event`: Runs when the popover wants to open or close. `details.reason` says why, and `details.cancel()` stops it.
|
|
1189
|
+
- `openchangecomplete event`: Runs once the popover has finished opening or closing, after any animation.
|
|
1190
|
+
- `payloadchange event`: Runs when the payload the content shows changes: a trigger with another payload opens the popover, or takes the open popover over, which fires no `openchange`, and so does a controller's `open({ payload })` on the open popover. `details.cancel()` keeps the previous payload.
|
|
1191
|
+
- `modal?` (`"trap-focus" | boolean`): `false`: the rest of the page stays usable, and focus leaving the popover closes it. `true`: also traps focus, locks scroll and makes the rest of the page inert, and an outside press only closes the popover. `"trap-focus"`: traps focus but leaves the page scrollable and clickable.
|
|
1192
|
+
- `openOnHover?` (`boolean`): Whether resting the pointer on the trigger opens the popover. Pressing the trigger still opens it, so keyboard and touch users can too.
|
|
1193
|
+
- `delay?` (`number`): Milliseconds the pointer rests on the trigger before the popover opens, with `openOnHover`.
|
|
1194
|
+
- `closeDelay?` (`number`): Milliseconds before the popover closes once the pointer leaves it and its trigger, with `openOnHover`.
|
|
1195
|
+
- `presentation?` (`PresentationSetting<"popover">`): How the popover presents. `"sheet"` slides it up from the bottom of the screen, where it can be swiped away, and `"auto"` does that on small touch screens. Decided each time it opens; the server renders `"popover"` for `"auto"`.
|
|
1196
|
+
- `controller?` (`PopupController<any, PopoverOpenChangeReason, {}, string, PopoverPayloadChangeReason>`): Opens, closes and inspects the popover from anywhere, and connects triggers outside it. Create one with `createPopoverController()`.
|
|
1197
|
+
- `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`.
|
|
1198
|
+
- `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.
|
|
1199
|
+
- `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
|
|
1200
|
+
|
|
1201
|
+
### PopoverTrigger
|
|
1202
|
+
|
|
1203
|
+
A trigger outside the popover names it with `for="popover-id"`, and takes `payload` as a property.
|
|
1204
|
+
|
|
1205
|
+
- `disabled?` (`boolean`): Stops the trigger from opening the popover. A native button can't be focused or pressed, unless `focusableWhenDisabled` is set.
|
|
1206
|
+
- `focusableWhenDisabled?` (`boolean`): Keeps a disabled trigger in the tab order with `aria-disabled` instead of the native attribute, so it can host a tooltip that says why. Presses still do nothing.
|
|
1207
|
+
- `payload?` (`unknown`): Data for the content to render when this trigger opens the popover, like the row it belongs to.
|
|
1208
|
+
- `shortcut?` (`string`): A keyboard shortcut that presses the trigger, like `"mod+i"`. `mod` is Command on Apple platforms and Control elsewhere. Sets `aria-keyshortcuts`.
|
|
1209
|
+
- `controller?` (`PopupController<any, PopoverOpenChangeReason, {}, string, PopoverPayloadChangeReason>`): Connects a trigger outside the popover, like one per table row, to the popover with this controller.
|
|
1210
|
+
|
|
1211
|
+
### PopoverContent
|
|
1212
|
+
|
|
1213
|
+
- `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 trigger the popover opens on.
|
|
1214
|
+
- `align?` (`Align`): How the popup lines up with the anchor along that side. `start` and `end` follow the reading direction. How the popover lines up with the trigger along that side.
|
|
1215
|
+
- `sideOffset?` (`number`): Gap between the anchor and the popup, in pixels. Gap between the trigger and the popover, in pixels.
|
|
1216
|
+
- `alignOffset?` (`number`): Shift along the anchor, in pixels. Shift along the trigger, in pixels.
|
|
1217
|
+
- `collisionPadding?` (`number`): Space to keep from the viewport edges, in pixels.
|
|
1218
|
+
- `arrowPadding?` (`number`): Space to keep between the arrow and the popup's corners, in pixels.
|
|
1219
|
+
- `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.
|
|
1220
|
+
- `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.
|
|
1221
|
+
- `initialFocus?` (`PopoverInitialFocus`): Where focus goes when the popover opens. Opening on hover never moves focus, and opening with touch focuses the popover itself so no on-screen keyboard appears.
|
|
1222
|
+
- `finalFocus?` (`PopupFocusTarget`): Where focus goes when the popover closes while focus is inside it.
|
|
1223
|
+
- `keepMounted?` (`boolean`): Whether the popover stays in the page, hidden, while closed. For animation libraries that drive the exit from state.
|
|
1224
|
+
|
|
1225
|
+
### PopoverArrow
|
|
1226
|
+
|
|
1227
|
+
Points at the trigger from the side the popover landed on.
|
|
1228
|
+
|
|
1229
|
+
### PopoverTitle
|
|
1230
|
+
|
|
1231
|
+
Names the popover. With `as-child`, it uses the heading inside it.
|
|
1232
|
+
|
|
1233
|
+
### PopoverDescription
|
|
1234
|
+
|
|
1235
|
+
Describes the popover while it's rendered.
|
|
1236
|
+
|
|
1237
|
+
### PopoverClose
|
|
1238
|
+
|
|
1239
|
+
Closes the popover. Wraps your own `button` or link.
|
|
1240
|
+
|
|
1241
|
+
### PopoverShortcut
|
|
1242
|
+
|
|
1243
|
+
- `shortcut?` (`string`): The keys to show. Inside a trigger, its `shortcut`.
|
|
1244
|
+
|
|
1245
|
+
### State
|
|
1246
|
+
|
|
1247
|
+
The same state shows as data attributes on each element.
|
|
1248
|
+
|
|
1249
|
+
`PopoverTrigger`:
|
|
1250
|
+
|
|
1251
|
+
- `open` (`boolean`): Whether the popover is open from this trigger.
|
|
1252
|
+
- `disabled` (`boolean`): Set by the trigger's `disabled` prop, which stops it opening the popover.
|
|
1253
|
+
|
|
1254
|
+
`PopoverContent`:
|
|
1255
|
+
|
|
1256
|
+
- `open` (`boolean`): Whether the popover is open. False while it animates out.
|
|
1257
|
+
- `modal` (`boolean | "trap-focus"`): The `modal` setting.
|
|
1258
|
+
- `presentation` (`"popover" | "sheet"`): How it presents this time it's open.
|
|
1259
|
+
- `payload` (`Payload | undefined`): The payload of the last open, from the trigger that opened it or a controller, typed as the controller's `Payload`.
|
|
1260
|
+
|
|
1261
|
+
`PopoverArrow`:
|
|
1262
|
+
|
|
1263
|
+
- `open` (`boolean`): Whether the popover is open. False as soon as it starts closing, while it still animates out.
|
|
1264
|
+
|
|
1265
|
+
`PopoverShortcut`:
|
|
1266
|
+
|
|
1267
|
+
- `text` (`string`): The shortcut formatted for this platform: `⌘I` on Apple platforms and `Ctrl+I` elsewhere. Empty without one.
|
|
1268
|
+
|
|
1269
|
+
### Reasons
|
|
1270
|
+
|
|
1271
|
+
`event.detail.reason` says why the popover opened or closed.
|
|
1272
|
+
|
|
1273
|
+
- `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`.
|
|
1274
|
+
- `trigger-hover` (`PointerEvent`): The pointer rested on a trigger, or left it, with hover opening.
|
|
1275
|
+
- `outside-press` (`MouseEvent`): A press outside the layer.
|
|
1276
|
+
- `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
|
|
1277
|
+
- `focus-out` (`KeyboardEvent | FocusEvent`): Focus left the layer, or Tab moved it out.
|
|
1278
|
+
- `window-blur` (`FocusEvent`): The window lost focus.
|
|
1279
|
+
- `close-press` (`MouseEvent | KeyboardEvent | SubmitEvent`): A Close part was pressed, or a form with `method="dialog"` was submitted.
|
|
1280
|
+
- `back` (`Event`): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
|
|
1281
|
+
- `shortcut` (`KeyboardEvent`): A keyboard shortcut, like a trigger's `shortcut`.
|
|
1282
|
+
- `swipe` (`TouchEvent | PointerEvent`): A swipe gesture: a finger, mouse or pen dragged it.
|
|
1283
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
1284
|
+
|
|
1285
|
+
For a payload change, it says what opened the popover or took it over.
|
|
1286
|
+
|
|
1287
|
+
- `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`.
|
|
1288
|
+
- `trigger-hover` (`PointerEvent`): The pointer rested on a trigger, or left it, with hover opening.
|
|
1289
|
+
- `shortcut` (`KeyboardEvent`): A keyboard shortcut, like a trigger's `shortcut`.
|
|
1290
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
1291
|
+
|
|
1292
|
+
With `dismissible` false, the refused dismissals `dismissattempt` reports:
|
|
1293
|
+
|
|
1294
|
+
- `outside-press` (`MouseEvent`): A press outside the layer.
|
|
1295
|
+
- `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
|
|
1296
|
+
- `back` (`Event`): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
|
|
1297
|
+
- `swipe` (`TouchEvent | PointerEvent`): A swipe gesture: a finger, mouse or pen dragged it.
|
|
1298
|
+
|
|
1299
|
+
### Controller
|
|
1300
|
+
|
|
1301
|
+
The element's `api` property has the same methods. The `open` property reads and sets whether the popover
|
|
1302
|
+
is open.
|
|
1303
|
+
|
|
1304
|
+
Every write method takes an optional `{ reason, event }`, passed to `openchange`.
|
|
1305
|
+
|
|
1306
|
+
- `isOpen()` (`() => boolean`): Whether it's open. False as soon as it starts closing, while it still animates out.
|
|
1307
|
+
- `setOpen(open, options?)` (`(open: boolean, options?: PopupChangeOptions<PopoverOpenChangeReason> | undefined) => boolean`): Opens or closes it. Returns false when the change was canceled or it was already open or closed.
|
|
1308
|
+
- `open(options?)` (`(options?: (PopupChangeOptions<PopoverPayloadChangeReason> & { payload?: Payload | undefined; anchor?: Anchor | undefined; signal?: AbortSignal | undefined; }) | undefined) => Promise<...>`): Opens it, and resolves once it closes, with why, how and any return value. Resolves exactly once and never rejects: on the next close from any source, when `signal` aborts, or with reason `"unmount"` when the component leaves the page.
|
|
1309
|
+
- `close(options?)` (`(options?: (PopupChangeOptions<PopoverOpenChangeReason> & { returnValue?: Result | undefined; }) | undefined) => boolean`): Closes it. `returnValue` is passed to the promise `open()` returned. Returns false when the close was canceled or it was already closed.
|
|
1310
|
+
- `toggle(options?)` (`(options?: PopupChangeOptions<PopoverOpenChangeReason> | undefined) => boolean`): Opens it when it's closed, and closes it when it's open. Returns false when the change was canceled.
|
|
1311
|
+
- `getPayload()` (`() => Payload | undefined`): The payload of the last open, from a controller or the trigger that opened it.
|
|
1312
|
+
- `focus()` (`() => void`): Moves focus to the trigger that opened it last, or the first trigger.
|
|
1313
|
+
|
|
1314
|
+
### Events
|
|
1315
|
+
|
|
1316
|
+
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 `openchange` cancels the change.
|
|
1317
|
+
|
|
1318
|
+
| Member | On | Description |
|
|
1319
|
+
| -------------------------- | -------------------- | --------------------------------------------------------------------------------------- |
|
|
1320
|
+
| `open` property, attribute | `ui-popover` | Whether the popover is open. The attribute sets where it starts, and is live |
|
|
1321
|
+
| `api` property | `ui-popover` | The [controller](#controller) |
|
|
1322
|
+
| `openchange` event | `ui-popover` | `detail: { value: open, reason, event, source }` |
|
|
1323
|
+
| `openchangecomplete` event | `ui-popover` | `detail: { value: open }`, after the animation |
|
|
1324
|
+
| `payloadchange` event | `ui-popover` | `detail: { value: payload, reason, event, source }`, when the content's payload changes |
|
|
1325
|
+
| `dismissattempt` event | `ui-popover` | `detail: { reason, event }`, when a dismissal was refused |
|
|
1326
|
+
| `payload` property | `ui-popover-trigger` | Data for the content, read when the trigger connects |
|
|
1327
|
+
| `for` attribute | `ui-popover-trigger` | The `id` of a popover the trigger opens from outside it |
|
|
1328
|
+
|
|
1329
|
+
## Accessibility
|
|
1330
|
+
|
|
1331
|
+
The trigger is a button that opens a dialog, and focus inside follows the [WAI-ARIA dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/).
|
|
1332
|
+
|
|
1333
|
+
### Tested Behavior
|
|
1334
|
+
|
|
1335
|
+
Every line below is an automated test.
|
|
1336
|
+
|
|
1337
|
+
**Trigger**
|
|
1338
|
+
|
|
1339
|
+
- Reflects the open state.
|
|
1340
|
+
- Click opens the popover.
|
|
1341
|
+
- Click on the open trigger closes the popover.
|
|
1342
|
+
- Enter opens the popover.
|
|
1343
|
+
- Space opens the popover.
|
|
1344
|
+
- Points to the popover it controls while open.
|
|
1345
|
+
|
|
1346
|
+
**Focus**
|
|
1347
|
+
|
|
1348
|
+
- Opening moves focus to the first focusable element.
|
|
1349
|
+
- Each time focus moves in, the popover is named by its title and described by its description.
|
|
1350
|
+
- Tab moves through the popover in order.
|
|
1351
|
+
- Shift+Tab from the first element returns focus to the trigger.
|
|
1352
|
+
- Tab from the last element moves focus to the element after the trigger.
|
|
1353
|
+
- Focus leaving the popover closes it.
|
|
1354
|
+
- Escape closes the popover and returns focus to the trigger.
|
|
1355
|
+
- Escape after a click returns focus to the trigger.
|
|
1356
|
+
- The close button closes the popover and returns focus to the trigger.
|
|
1357
|
+
- With initial focus none, opening leaves focus on the trigger.
|
|
1358
|
+
- A virtual click from a screen reader moves focus into the popover.
|
|
1359
|
+
- A touch press focuses the popover itself, so no on-screen keyboard appears.
|
|
1360
|
+
- Closing after the trigger is gone moves focus to the element after where it was.
|
|
1361
|
+
- Pressing a button inside keeps the popover open.
|
|
1362
|
+
|
|
1363
|
+
**Dismissal**
|
|
1364
|
+
|
|
1365
|
+
- Clicking outside closes the popover.
|
|
1366
|
+
- Clicking inside the popover keeps it open.
|
|
1367
|
+
- An outside press reaches what it hits.
|
|
1368
|
+
- A press on empty space returns focus to the trigger.
|
|
1369
|
+
- Escape while an input method is composing text leaves the popover open.
|
|
1370
|
+
- Pressing a scrollbar outside the popover leaves it open.
|
|
1371
|
+
- Pressing a button in a shadow root inside keeps the popover open.
|
|
1372
|
+
- Tab into a button in a shadow root inside keeps the popover open.
|
|
1373
|
+
- Escape on a button in a shadow root inside returns focus to the trigger.
|
|
1374
|
+
- Switching windows leaves the popover open.
|
|
1375
|
+
|
|
1376
|
+
**Modal**
|
|
1377
|
+
|
|
1378
|
+
- With modal true, Tab stays inside the popover.
|
|
1379
|
+
- With modal trap-focus, Tab stays inside the popover.
|
|
1380
|
+
- With modal true, an outside press only closes the popover.
|
|
1381
|
+
- With modal true, the page doesn't scroll while open.
|
|
1382
|
+
|
|
1383
|
+
**Hover**
|
|
1384
|
+
|
|
1385
|
+
- Resting the pointer on the trigger opens the popover.
|
|
1386
|
+
- Moving the pointer into the popover keeps it open.
|
|
1387
|
+
- Moving the pointer away closes the popover.
|
|
1388
|
+
- Opening on hover leaves focus where it was.
|
|
1389
|
+
- Keyboard focus on the trigger doesn't open the popover.
|
|
1390
|
+
- Escape on a popover opened by hover leaves focus where it was.
|
|
1391
|
+
- Pressing the trigger still opens the popover.
|
|
1392
|
+
|
|
1393
|
+
**Nested popovers**
|
|
1394
|
+
|
|
1395
|
+
- Using a nested popover keeps both open.
|
|
1396
|
+
- Escape closes only the nested popover and returns focus to its trigger.
|
|
1397
|
+
- Clicking outside closes every popover.
|
|
1398
|
+
|
|
1399
|
+
Runs in React, Vue and web components.
|
|
1400
|
+
|
|
1401
|
+
Additional behaviors:
|
|
1402
|
+
|
|
1403
|
+
**Enhancements**
|
|
1404
|
+
|
|
1405
|
+
- The popup writes --arrow-offset, where the trigger's center falls along its edge.
|
|
1406
|
+
- Without a title, the trigger names the popover.
|
|
1407
|
+
- Tab from the trigger of a popover opened by hover moves into it.
|
|
1408
|
+
- A controller opens the popover from anywhere and moves focus in.
|
|
1409
|
+
- A popover open from the start leaves focus alone.
|
|
1410
|
+
- Modal popovers make the rest of the page inert and restore it exactly.
|
|
1411
|
+
- Live regions keep announcing while a modal popover makes the page inert.
|
|
1412
|
+
- Clicking into an iframe closes the popover.
|
|
1413
|
+
- An infinite exit animation still unmounts.
|
|
1414
|
+
|
|
1415
|
+
**Shortcuts**
|
|
1416
|
+
|
|
1417
|
+
- A trigger's shortcut opens and closes the popover.
|
|
1418
|
+
- The trigger advertises its shortcut, and its Shortcut part shows the keys to sighted users only.
|
|
1419
|
+
- A Shortcut part shows its own keys.
|
|
1420
|
+
- A trigger connected through a controller shows and binds its own shortcut.
|
|
1421
|
+
|
|
1422
|
+
**Web components**
|
|
1423
|
+
|
|
1424
|
+
- A title added after the popover connects names it. Web components only.
|
|
1425
|
+
- Changing an attribute updates the part. Web components only.
|
|
1426
|
+
|
|
1427
|
+
Runs in React, Vue and web components.
|
|
1428
|
+
|
|
1429
|
+
Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:
|
|
1430
|
+
|
|
1431
|
+
- Announcements while using the popover with a keyboard.
|
|
1432
|
+
|
|
1433
|
+
Runs in React, Vue and web components.
|