@threadlabs/looma 0.14.7 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/components/shared/icons.js +5 -0
  2. package/components/shared/menu-navigation.js +49 -0
  3. package/components/shared/overlay.js +33 -60
  4. package/components/ui-avatar/ui-avatar.html +19 -2
  5. package/components/ui-avatar/ui-avatar.js +7 -1
  6. package/components/ui-avatar-group/ui-avatar-group.html +8 -3
  7. package/components/ui-avatar-group/ui-avatar-group.js +6 -2
  8. package/components/ui-badge/ui-badge.html +70 -4
  9. package/components/ui-button/ui-button.html +21 -5
  10. package/components/ui-button/ui-button.js +10 -0
  11. package/components/ui-callout/ui-callout.html +13 -5
  12. package/components/ui-checkbox/ui-checkbox.html +11 -0
  13. package/components/ui-combobox/ui-combobox.html +50 -10
  14. package/components/ui-combobox/ui-combobox.js +11 -8
  15. package/components/ui-context-menu/ui-context-menu.html +33 -5
  16. package/components/ui-context-menu/ui-context-menu.js +26 -17
  17. package/components/ui-dialog/ui-dialog.html +51 -8
  18. package/components/ui-dialog/ui-dialog.js +28 -7
  19. package/components/ui-disclosure/ui-disclosure.html +14 -5
  20. package/components/ui-disclosure/ui-disclosure.js +51 -4
  21. package/components/ui-editable/ui-editable.html +17 -3
  22. package/components/ui-editable/ui-editable.js +7 -0
  23. package/components/ui-editor-insert-table-grid/ui-editor-insert-table-grid.html +48 -14
  24. package/components/ui-editor-insert-table-grid/ui-editor-insert-table-grid.js +40 -6
  25. package/components/ui-editor-mention-menu/ui-editor-mention-menu.html +22 -9
  26. package/components/ui-editor-mention-menu/ui-editor-mention-menu.js +33 -11
  27. package/components/ui-editor-mention-menu-item/ui-editor-mention-menu-item.html +67 -0
  28. package/components/ui-editor-slash-menu/ui-editor-slash-menu.html +33 -30
  29. package/components/ui-editor-slash-menu/ui-editor-slash-menu.js +42 -8
  30. package/components/ui-editor-slash-menu-group/ui-editor-slash-menu-group.html +31 -0
  31. package/components/ui-editor-slash-menu-item/ui-editor-slash-menu-item.html +75 -0
  32. package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.html +16 -5
  33. package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.js +10 -2
  34. package/components/ui-editor-table-overlay/ui-editor-table-overlay.html +5 -5
  35. package/components/ui-editor-table-overlay/ui-editor-table-overlay.js +5 -2
  36. package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.html +14 -4
  37. package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.js +2 -2
  38. package/components/ui-editor-toolbar/ui-editor-toolbar.html +1 -1
  39. package/components/ui-editor-toolbar/ui-editor-toolbar.js +46 -0
  40. package/components/ui-floating-action-button/ui-floating-action-button.html +1 -1
  41. package/components/ui-form-field/ui-form-field.html +16 -3
  42. package/components/ui-form-field/ui-form-field.js +1 -1
  43. package/components/ui-grid/ui-grid.html +17 -2
  44. package/components/ui-icon/ui-icon.html +5 -0
  45. package/components/ui-icon-button/ui-icon-button.html +25 -6
  46. package/components/ui-icon-button/ui-icon-button.js +11 -1
  47. package/components/ui-input/ui-input.html +3 -3
  48. package/components/ui-input-group/ui-input-group.html +18 -4
  49. package/components/ui-listbox/ui-listbox.html +81 -0
  50. package/components/ui-listbox/ui-listbox.js +27 -0
  51. package/components/ui-menu/ui-menu.html +33 -5
  52. package/components/ui-menu/ui-menu.js +18 -12
  53. package/components/ui-menu-group/ui-menu-group.html +27 -0
  54. package/components/ui-menu-item/ui-menu-item.html +80 -8
  55. package/components/ui-menu-item/ui-menu-item.js +47 -0
  56. package/components/ui-popover/ui-popover.html +33 -5
  57. package/components/ui-popover/ui-popover.js +9 -1
  58. package/components/ui-radio-group/ui-radio-group.html +13 -1
  59. package/components/ui-search-shell/ui-search-shell.html +6 -6
  60. package/components/ui-search-shell/ui-search-shell.js +21 -5
  61. package/components/ui-select/ui-select.html +14 -4
  62. package/components/ui-switch/ui-switch.html +2 -2
  63. package/components/ui-switcher/ui-switcher.html +8 -1
  64. package/components/ui-switcher/ui-switcher.js +15 -0
  65. package/components/ui-tabs/ui-tabs.html +17 -12
  66. package/components/ui-tabs/ui-tabs.js +43 -7
  67. package/components/ui-textarea/ui-textarea.html +28 -5
  68. package/components/ui-toast/ui-toast.html +88 -0
  69. package/components/ui-toast/ui-toast.js +68 -0
  70. package/components/ui-toast-region/ui-toast-region.html +102 -19
  71. package/components/ui-toast-region/ui-toast-region.js +10 -18
  72. package/components/ui-tooltip/ui-tooltip.html +51 -7
  73. package/components/ui-tooltip/ui-tooltip.js +29 -3
  74. package/components/ui-tree/ui-tree.html +12 -0
  75. package/components/ui-tree/ui-tree.js +191 -12
  76. package/components/ui-tree-item/ui-tree-item.html +87 -8
  77. package/components/ui-tree-item/ui-tree-item.js +3 -1
  78. package/dist/index.js +38 -32
  79. package/editor/chunks/extensions.js +82 -2
  80. package/editor/extensions/table-commands.d.ts +8 -0
  81. package/editor/icons.d.ts +5 -0
  82. package/editor/index.js +7 -2
  83. package/editor/insert-table-grid.d.ts +1 -1
  84. package/editor/mention-menu.d.ts +2 -0
  85. package/editor/slash-menu.d.ts +3 -0
  86. package/editor/table-context-menu.d.ts +1 -1
  87. package/editor/table-overlay.d.ts +2 -2
  88. package/package.json +1 -1
  89. package/styles/ui-avatar-group.css +1 -2
  90. package/styles/ui-avatar.css +11 -1
  91. package/styles/ui-badge.css +73 -1
  92. package/styles/ui-button.css +9 -1
  93. package/styles/ui-callout.css +1 -1
  94. package/styles/ui-checkbox.css +9 -0
  95. package/styles/ui-combobox.css +38 -7
  96. package/styles/ui-context-menu.css +32 -4
  97. package/styles/ui-dialog.css +42 -5
  98. package/styles/ui-editor-insert-table-grid.css +48 -11
  99. package/styles/ui-editor-mention-menu-item.css +57 -0
  100. package/styles/ui-editor-mention-menu.css +11 -1
  101. package/styles/ui-editor-slash-menu-group.css +21 -0
  102. package/styles/ui-editor-slash-menu-item.css +62 -0
  103. package/styles/ui-editor-slash-menu.css +10 -21
  104. package/styles/ui-editor-table-context-menu.css +1 -1
  105. package/styles/ui-floating-action-button.css +1 -1
  106. package/styles/ui-form-field.css +14 -1
  107. package/styles/ui-grid.css +14 -1
  108. package/styles/ui-icon-button.css +17 -5
  109. package/styles/ui-input-group.css +18 -4
  110. package/styles/ui-input.css +3 -3
  111. package/styles/ui-listbox.css +69 -0
  112. package/styles/ui-menu-group.css +19 -0
  113. package/styles/ui-menu-item.css +48 -9
  114. package/styles/ui-menu.css +32 -4
  115. package/styles/ui-popover.css +28 -4
  116. package/styles/ui-radio-group.css +9 -0
  117. package/styles/ui-search-shell.css +5 -5
  118. package/styles/ui-select.css +14 -4
  119. package/styles/ui-switch.css +2 -2
  120. package/styles/ui-switcher.css +4 -0
  121. package/styles/ui-textarea.css +27 -4
  122. package/styles/ui-toast-region.css +88 -4
  123. package/styles/ui-toast.css +74 -0
  124. package/styles/ui-tooltip.css +46 -6
  125. package/styles/ui-tree-item.css +72 -4
  126. package/styles/ui-tree.css +8 -0
  127. package/tokens.css +3 -0
  128. package/vanilla/UiAvatar.d.ts +2 -0
  129. package/vanilla/UiAvatar.js +8 -1
  130. package/vanilla/UiAvatarGroup.d.ts +2 -0
  131. package/vanilla/UiAvatarGroup.js +1 -1
  132. package/vanilla/UiBadge.d.ts +1 -1
  133. package/vanilla/UiBadge.js +1 -1
  134. package/vanilla/UiButton.d.ts +1 -0
  135. package/vanilla/UiButton.js +22 -6
  136. package/vanilla/UiCallout.d.ts +1 -1
  137. package/vanilla/UiCallout.js +1 -1
  138. package/vanilla/UiCheckbox.d.ts +1 -0
  139. package/vanilla/UiCheckbox.js +12 -9
  140. package/vanilla/UiCombobox.d.ts +4 -0
  141. package/vanilla/UiCombobox.js +30 -15
  142. package/vanilla/UiContextMenu.d.ts +1 -1
  143. package/vanilla/UiContextMenu.js +1 -1
  144. package/vanilla/UiDialog.d.ts +5 -2
  145. package/vanilla/UiDialog.js +1 -1
  146. package/vanilla/UiDisclosure.d.ts +2 -0
  147. package/vanilla/UiDisclosure.js +32 -18
  148. package/vanilla/UiEditable.d.ts +5 -0
  149. package/vanilla/UiEditable.js +7 -3
  150. package/vanilla/UiEditorInsertTableGrid.js +1 -6
  151. package/vanilla/UiEditorMentionMenu.d.ts +4 -2
  152. package/vanilla/UiEditorMentionMenu.js +49 -31
  153. package/vanilla/UiEditorMentionMenuItem.d.ts +16 -0
  154. package/vanilla/UiEditorMentionMenuItem.js +56 -0
  155. package/vanilla/UiEditorSlashMenu.d.ts +4 -3
  156. package/vanilla/UiEditorSlashMenu.js +59 -46
  157. package/vanilla/UiEditorSlashMenuGroup.d.ts +14 -0
  158. package/vanilla/UiEditorSlashMenuGroup.js +48 -0
  159. package/vanilla/UiEditorSlashMenuItem.d.ts +18 -0
  160. package/vanilla/UiEditorSlashMenuItem.js +57 -0
  161. package/vanilla/UiEditorTableContextMenu.d.ts +5 -2
  162. package/vanilla/UiEditorTableContextMenu.js +1 -2
  163. package/vanilla/UiEditorTableOverlay.d.ts +1 -1
  164. package/vanilla/UiEditorTableOverlay.js +5 -5
  165. package/vanilla/UiEditorTableToolbar.d.ts +4 -2
  166. package/vanilla/UiEditorTableToolbar.js +1 -2
  167. package/vanilla/UiEditorToolbar.js +2 -0
  168. package/vanilla/UiGrid.d.ts +1 -0
  169. package/vanilla/UiGrid.js +8 -7
  170. package/vanilla/UiIcon.js +1 -1
  171. package/vanilla/UiIconButton.d.ts +1 -0
  172. package/vanilla/UiIconButton.js +11 -2
  173. package/vanilla/UiListbox.d.ts +20 -0
  174. package/vanilla/UiListbox.js +45 -0
  175. package/vanilla/UiMenu.d.ts +1 -1
  176. package/vanilla/UiMenu.js +1 -1
  177. package/vanilla/UiMenuGroup.d.ts +14 -0
  178. package/vanilla/UiMenuGroup.js +35 -0
  179. package/vanilla/UiMenuItem.d.ts +5 -1
  180. package/vanilla/UiMenuItem.js +91 -30
  181. package/vanilla/UiPopover.d.ts +1 -1
  182. package/vanilla/UiPopover.js +1 -1
  183. package/vanilla/UiRadioGroup.d.ts +1 -0
  184. package/vanilla/UiRadioGroup.js +6 -3
  185. package/vanilla/UiSwitcher.d.ts +1 -0
  186. package/vanilla/UiSwitcher.js +7 -9
  187. package/vanilla/UiTabs.d.ts +1 -0
  188. package/vanilla/UiTabs.js +9 -1
  189. package/vanilla/UiTextarea.d.ts +1 -0
  190. package/vanilla/UiTextarea.js +4 -1
  191. package/vanilla/UiToast.d.ts +16 -0
  192. package/vanilla/UiToast.js +53 -0
  193. package/vanilla/UiToastRegion.d.ts +1 -4
  194. package/vanilla/UiToastRegion.js +3 -11
  195. package/vanilla/UiTooltip.d.ts +1 -1
  196. package/vanilla/UiTooltip.js +1 -1
  197. package/vanilla/UiTree.d.ts +2 -0
  198. package/vanilla/UiTree.js +1 -1
  199. package/vanilla/UiTreeItem.d.ts +1 -0
  200. package/vanilla/UiTreeItem.js +45 -31
  201. package/vanilla/index.js +6 -0
  202. package/vue/UiAvatar.d.ts +4 -0
  203. package/vue/UiAvatar.vue +23 -3
  204. package/vue/UiAvatarGroup.d.ts +3 -0
  205. package/vue/UiAvatarGroup.vue +7 -4
  206. package/vue/UiBadge.d.ts +2 -2
  207. package/vue/UiBadge.vue +74 -2
  208. package/vue/UiButton.d.ts +5 -3
  209. package/vue/UiButton.vue +41 -7
  210. package/vue/UiCallout.d.ts +2 -2
  211. package/vue/UiCallout.vue +3 -2
  212. package/vue/UiCheckbox.d.ts +2 -0
  213. package/vue/UiCheckbox.vue +16 -0
  214. package/vue/UiCombobox.d.ts +13 -3
  215. package/vue/UiCombobox.vue +62 -11
  216. package/vue/UiContextMenu.d.ts +2 -0
  217. package/vue/UiContextMenu.vue +47 -6
  218. package/vue/UiDialog.d.ts +18 -4
  219. package/vue/UiDialog.vue +86 -11
  220. package/vue/UiDisclosure.d.ts +8 -2
  221. package/vue/UiDisclosure.vue +38 -14
  222. package/vue/UiEditable.d.ts +10 -0
  223. package/vue/UiEditable.vue +14 -2
  224. package/vue/UiEditorInsertTableGrid.vue +52 -15
  225. package/vue/UiEditorMentionMenu.d.ts +24 -2
  226. package/vue/UiEditorMentionMenu.vue +27 -11
  227. package/vue/UiEditorMentionMenuItem.d.ts +24 -0
  228. package/vue/UiEditorMentionMenuItem.js +2 -0
  229. package/vue/UiEditorMentionMenuItem.vue +88 -0
  230. package/vue/UiEditorSlashMenu.d.ts +24 -1
  231. package/vue/UiEditorSlashMenu.vue +33 -33
  232. package/vue/UiEditorSlashMenuGroup.d.ts +18 -0
  233. package/vue/UiEditorSlashMenuGroup.js +2 -0
  234. package/vue/UiEditorSlashMenuGroup.vue +43 -0
  235. package/vue/UiEditorSlashMenuItem.d.ts +26 -0
  236. package/vue/UiEditorSlashMenuItem.js +2 -0
  237. package/vue/UiEditorSlashMenuItem.vue +101 -0
  238. package/vue/UiEditorTableContextMenu.d.ts +9 -3
  239. package/vue/UiEditorTableContextMenu.vue +53 -3
  240. package/vue/UiEditorTableOverlay.d.ts +2 -2
  241. package/vue/UiEditorTableOverlay.vue +8 -6
  242. package/vue/UiEditorTableToolbar.d.ts +7 -3
  243. package/vue/UiEditorTableToolbar.vue +50 -2
  244. package/vue/UiEditorToolbar.vue +14 -1
  245. package/vue/UiFloatingActionButton.vue +1 -1
  246. package/vue/UiFormField.vue +28 -3
  247. package/vue/UiGrid.d.ts +1 -0
  248. package/vue/UiGrid.vue +26 -2
  249. package/vue/UiIcon.vue +16 -0
  250. package/vue/UiIconButton.d.ts +5 -3
  251. package/vue/UiIconButton.vue +40 -6
  252. package/vue/UiInput.vue +10 -3
  253. package/vue/UiInputGroup.vue +32 -11
  254. package/vue/UiListbox.d.ts +33 -0
  255. package/vue/UiListbox.js +2 -0
  256. package/vue/UiListbox.vue +131 -0
  257. package/vue/UiMenu.d.ts +2 -0
  258. package/vue/UiMenu.vue +43 -6
  259. package/vue/UiMenuGroup.d.ts +18 -0
  260. package/vue/UiMenuGroup.js +2 -0
  261. package/vue/UiMenuGroup.vue +39 -0
  262. package/vue/UiMenuItem.d.ts +34 -2
  263. package/vue/UiMenuItem.vue +145 -9
  264. package/vue/UiPopover.d.ts +2 -2
  265. package/vue/UiPopover.vue +44 -5
  266. package/vue/UiRadioGroup.d.ts +2 -0
  267. package/vue/UiRadioGroup.vue +17 -0
  268. package/vue/UiSearchShell.vue +12 -5
  269. package/vue/UiSelect.vue +18 -4
  270. package/vue/UiSwitch.vue +2 -2
  271. package/vue/UiSwitcher.d.ts +1 -0
  272. package/vue/UiSwitcher.vue +22 -1
  273. package/vue/UiTabs.d.ts +6 -2
  274. package/vue/UiTabs.vue +14 -12
  275. package/vue/UiTextarea.d.ts +2 -0
  276. package/vue/UiTextarea.vue +32 -4
  277. package/vue/UiToast.d.ts +34 -0
  278. package/vue/UiToast.js +2 -0
  279. package/vue/UiToast.vue +173 -0
  280. package/vue/UiToastRegion.d.ts +2 -18
  281. package/vue/UiToastRegion.vue +123 -48
  282. package/vue/UiTooltip.d.ts +2 -2
  283. package/vue/UiTooltip.vue +67 -12
  284. package/vue/UiTree.d.ts +10 -0
  285. package/vue/UiTree.vue +27 -0
  286. package/vue/UiTreeItem.d.ts +2 -0
  287. package/vue/UiTreeItem.vue +96 -4
  288. package/vue/chunks/UiAvatar.js +19 -5
  289. package/vue/chunks/UiAvatarGroup.js +10 -4
  290. package/vue/chunks/UiBadge.js +1 -1
  291. package/vue/chunks/UiButton.js +58 -11
  292. package/vue/chunks/UiCallout.js +2 -2
  293. package/vue/chunks/UiCheckbox.js +12 -2
  294. package/vue/chunks/UiCombobox.js +30 -13
  295. package/vue/chunks/UiContextMenu.js +6 -2
  296. package/vue/chunks/UiDialog.js +25 -8
  297. package/vue/chunks/UiDisclosure.js +36 -12
  298. package/vue/chunks/UiEditable.js +22 -8
  299. package/vue/chunks/UiEditorInsertTableGrid.js +11 -14
  300. package/vue/chunks/UiEditorMentionMenu.js +30 -19
  301. package/vue/chunks/UiEditorMentionMenuItem.js +36 -0
  302. package/vue/chunks/UiEditorSlashMenu.js +33 -35
  303. package/vue/chunks/UiEditorSlashMenuGroup.js +26 -0
  304. package/vue/chunks/UiEditorSlashMenuItem.js +51 -0
  305. package/vue/chunks/UiEditorTableContextMenu.js +39 -5
  306. package/vue/chunks/UiEditorTableOverlay.js +6 -6
  307. package/vue/chunks/UiEditorTableToolbar.js +37 -4
  308. package/vue/chunks/UiEditorToolbar.js +18 -4
  309. package/vue/chunks/UiFloatingActionButton.js +1 -1
  310. package/vue/chunks/UiFormField.js +13 -9
  311. package/vue/chunks/UiGrid.js +6 -2
  312. package/vue/chunks/UiIcon.js +40 -2
  313. package/vue/chunks/UiIconButton.js +35 -4
  314. package/vue/chunks/UiInput.js +1 -1
  315. package/vue/chunks/UiInputGroup.js +1 -1
  316. package/vue/chunks/UiListbox.js +70 -0
  317. package/vue/chunks/UiMenu.js +6 -2
  318. package/vue/chunks/UiMenuGroup.js +25 -0
  319. package/vue/chunks/UiMenuItem.js +128 -7
  320. package/vue/chunks/UiPopover.js +1 -1
  321. package/vue/chunks/UiRadioGroup.js +8 -2
  322. package/vue/chunks/UiSearchShell.js +1 -1
  323. package/vue/chunks/UiSelect.js +1 -1
  324. package/vue/chunks/UiSwitch.js +1 -1
  325. package/vue/chunks/UiSwitcher.js +20 -3
  326. package/vue/chunks/UiTabs.js +4 -3
  327. package/vue/chunks/UiTextarea.js +7 -2
  328. package/vue/chunks/UiToast.js +81 -0
  329. package/vue/chunks/UiToastRegion.js +25 -49
  330. package/vue/chunks/UiTooltip.js +3 -7
  331. package/vue/chunks/UiTree.js +8 -4
  332. package/vue/chunks/UiTreeItem.js +28 -10
  333. package/vue/components.css +1840 -851
  334. package/vue/editor/index.js +263 -2
  335. package/vue/index.d.ts +6 -0
  336. package/vue/index.js +9 -3
@@ -3,6 +3,7 @@
3
3
  <prop name="checked" type="boolean" default="false"
4
4
  >Whether the checkbox is checked initially or when set externally. A form reset returns it to this.</prop>
5
5
  <prop name="disabled" type="boolean" default="false">Disables the native checkbox, so it cannot be toggled or focused.</prop>
6
+ <prop name="invalid" type="boolean" default="false">Exposes an externally validated error and styles the checkbox with the field danger colour.</prop>
6
7
  <prop name="indeterminate" type="boolean" default="false"
7
8
  >Shows the mixed state, for a checkbox that summarises a group whose items are partly checked. It sets the native indeterminate property, which is
8
9
  independent of checked, so update it as the group changes.</prop>
@@ -28,6 +29,7 @@
28
29
  .checked="internalChecked"
29
30
  .indeterminate="indeterminate"
30
31
  :disabled="disabled"
32
+ :aria-invalid="invalid"
31
33
  :required="required"
32
34
  :name="name"
33
35
  :value="value"
@@ -43,6 +45,10 @@
43
45
  syntax: "*";
44
46
  inherits: false;
45
47
  }
48
+ @property --ui-checkbox-invalid-border {
49
+ syntax: "*";
50
+ inherits: false;
51
+ }
46
52
  @property --ui-checkbox-size {
47
53
  syntax: "*";
48
54
  inherits: false;
@@ -51,6 +57,7 @@
51
57
  :host {
52
58
  /* Hooks do not inherit, so the parts below read them from the root. */
53
59
  --_ui-checkbox-size: var(--ui-checkbox-size);
60
+ --_ui-checkbox-invalid-border: var(--ui-checkbox-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)));
54
61
 
55
62
  display: inline-flex;
56
63
  align-items: flex-start;
@@ -148,6 +155,10 @@
148
155
  outline-offset: 2px;
149
156
  }
150
157
 
158
+ :host-state([invalid]) input {
159
+ border-color: var(--_ui-checkbox-invalid-border);
160
+ }
161
+
151
162
  :host-state([disabled]) {
152
163
  color: var(--ui-disabled-text);
153
164
  cursor: not-allowed;
@@ -33,6 +33,10 @@
33
33
  <prop name="query" type="string"
34
34
  >The input's text, when the consumer controls it. Once set, the consumer owns the text: typing reports query-change, and the input returns to query unless
35
35
  query is updated to match. Leave it unset to let the combobox manage its own text, showing the chosen option's label.</prop>
36
+ <prop name="filter" type="label | none" default="label"
37
+ >How the list narrows as the user types. label lists the options whose label contains the typed text, ignoring case. none lists every authored option, for
38
+ options already narrowed elsewhere, such as a server search, so a result that matched on data other than its label still shows. Leaving the field still
39
+ commits an option whose label matches the text, or the only one listed.</prop>
36
40
  <prop name="allowFreeText" type="boolean" default="false">Allows values that do not match an authored option.</prop>
37
41
  <prop name="allowCreate" type="boolean" default="false"
38
42
  >Offers the current query as a new option, reported as create-item. When nothing else matches, the offer is highlighted, so Enter creates it; with nothing
@@ -46,6 +50,11 @@
46
50
  <prop name="required" type="boolean" default="false"
47
51
  >Requires a value: adds an asterisk to the label, marks the input required for native form validation, and makes validation report "A value is required."
48
52
  when the field is left empty. In multiple mode a chosen item satisfies it, whatever is left in the input.</prop>
53
+ <prop name="invalid" type="boolean" default="false"
54
+ >Exposes an externally validated error on the input and field. It remains until the consumer clears it, independently of native validation.</prop>
55
+ <prop name="openOnFocus" type="boolean" default="false">Opens suggestions when the input receives focus, before typing.</prop>
56
+ <prop name="loading" type="boolean" default="false"
57
+ >Marks the suggestion list busy while the consumer loads options. It announces loading and suppresses the empty state and create offer until cleared.</prop>
49
58
  <prop name="size" type="sm | md | lg" default="md"
50
59
  >Field height, padding, and type size, matching Button's sizes so a field and a button of one size share a row. sm is the small control height (2rem by
51
60
  default) with smaller text, for compact rows such as a table row or a filter bar; md is the standard control height; lg is taller with roomier padding.
@@ -77,7 +86,6 @@
77
86
  :value="[]"
78
87
  ></state>
79
88
  <state name="active" :value="-1"></state>
80
- <state name="loading" :value="false"></state>
81
89
  <state name="helpOpen" :value="false"></state>
82
90
  <state name="lookupError" :value="''"></state>
83
91
  <state
@@ -159,8 +167,8 @@
159
167
  autocomplete="off"
160
168
  :aria-controls="format('%s-listbox', uid)"
161
169
  :aria-expanded="expanded"
162
- :aria-invalid="validationStatus = 'error'"
163
- :aria-busy="validationStatus = 'pending'"
170
+ :aria-invalid="invalid or validationStatus = 'error'"
171
+ :aria-busy="loading or validationStatus = 'pending'"
164
172
  .value="display"
165
173
  :placeholder="placeholder"
166
174
  :disabled="disabled"
@@ -258,6 +266,7 @@
258
266
  >
259
267
  </div>
260
268
  <div $if="message" class="message" $value="message"></div>
269
+ <div $if="not loading and not rows[0] and not creatable and not lookupError" class="message"><slot name="empty">No suggestions.</slot></div>
261
270
  </div>
262
271
  <slot name="footer"></slot>
263
272
  </div>
@@ -309,6 +318,30 @@
309
318
  syntax: "*";
310
319
  inherits: false;
311
320
  }
321
+ @property --ui-combobox-radius {
322
+ syntax: "*";
323
+ inherits: false;
324
+ }
325
+ @property --ui-combobox-invalid-border {
326
+ syntax: "*";
327
+ inherits: false;
328
+ }
329
+ @property --ui-combobox-popup-surface {
330
+ syntax: "*";
331
+ inherits: false;
332
+ }
333
+ @property --ui-combobox-popup-border {
334
+ syntax: "*";
335
+ inherits: false;
336
+ }
337
+ @property --ui-combobox-popup-radius {
338
+ syntax: "*";
339
+ inherits: false;
340
+ }
341
+ @property --ui-combobox-popup-shadow {
342
+ syntax: "*";
343
+ inherits: false;
344
+ }
312
345
 
313
346
  :host {
314
347
  /* Hooks do not inherit, so the parts below read them from the root. */
@@ -322,6 +355,12 @@
322
355
  --_ui-combobox-item-min-block-size: var(--ui-combobox-item-min-block-size);
323
356
  --_ui-combobox-item-input-min-inline-size: var(--ui-combobox-item-input-min-inline-size);
324
357
  --_ui-combobox-item-item-max-inline-size: var(--ui-combobox-item-item-max-inline-size);
358
+ --_ui-combobox-radius: var(--ui-combobox-radius);
359
+ --_ui-combobox-invalid-border: var(--ui-combobox-invalid-border);
360
+ --_ui-combobox-popup-surface: var(--ui-combobox-popup-surface);
361
+ --_ui-combobox-popup-border: var(--ui-combobox-popup-border);
362
+ --_ui-combobox-popup-radius: var(--ui-combobox-popup-radius);
363
+ --_ui-combobox-popup-shadow: var(--ui-combobox-popup-shadow);
325
364
 
326
365
  display: grid;
327
366
  gap: var(--ui-space-1);
@@ -348,7 +387,7 @@
348
387
  display: flex;
349
388
  min-inline-size: 0;
350
389
  border: 1px solid var(--ui-control-border, var(--ui-border-default));
351
- border-radius: var(--ui-radius-md);
390
+ border-radius: var(--_ui-combobox-radius, var(--ui-field-radius, var(--ui-radius-md)));
352
391
  background: var(--ui-control-surface, var(--ui-surface-default));
353
392
  box-shadow: var(--ui-control-inset);
354
393
  transition: border-color var(--ui-motion-fast) var(--ui-motion-ease), box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
@@ -466,8 +505,9 @@
466
505
  .field:has(input:disabled) {
467
506
  background: var(--ui-control-surface-disabled, var(--ui-surface-muted));
468
507
  }
469
- :host-state([validationStatus="error"]) .field {
470
- border-color: var(--ui-danger-solid, var(--ui-accent-active));
508
+ :host-state([validationStatus="error"]) .field,
509
+ :host-state([invalid]) .field {
510
+ border-color: var(--_ui-combobox-invalid-border, var(--ui-field-danger, var(--ui-danger-solid, var(--ui-accent-active))));
471
511
  }
472
512
  .popup {
473
513
  box-sizing: border-box;
@@ -477,11 +517,11 @@
477
517
  overflow: auto;
478
518
  margin: 0;
479
519
  padding: var(--ui-space-1);
480
- border: 1px solid var(--ui-border-default);
481
- border-radius: var(--ui-radius-md);
482
- background: var(--ui-surface-elevated);
520
+ border: 1px solid var(--_ui-combobox-popup-border, var(--ui-overlay-border, var(--ui-border-default)));
521
+ border-radius: var(--_ui-combobox-popup-radius, var(--ui-overlay-radius, var(--ui-radius-md)));
522
+ background: var(--_ui-combobox-popup-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
483
523
  color: var(--ui-text-primary);
484
- box-shadow: var(--ui-elevation-overlay, var(--ui-shadow-lg));
524
+ box-shadow: var(--_ui-combobox-popup-shadow, var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg))));
485
525
  }
486
526
  [hidden] {
487
527
  display: none;
@@ -55,6 +55,8 @@ export default function controller(host) {
55
55
  options: authoredOptions(authored),
56
56
  allowFreeText: Boolean(host.state.allowFreeText),
57
57
  allowCreate: Boolean(host.state.allowCreate),
58
+ // "none": the consumer already narrowed the options (a server search), so every one is listed.
59
+ filterByLabel: host.state.filter !== "none",
58
60
  });
59
61
  // Selected items work uncontrolled: the component keeps them and reports every change. A consumer
60
62
  // that owns `items` stays in charge, because the prop resyncs whatever it sets. A consumer that sets
@@ -108,7 +110,6 @@ export default function controller(host) {
108
110
  const cancelLookup = () => {
109
111
  lookup?.abort();
110
112
  lookup = undefined;
111
- host.state.loading = false;
112
113
  };
113
114
  const close = () => {
114
115
  host.state.expanded = false;
@@ -149,7 +150,7 @@ export default function controller(host) {
149
150
  const filtered = options.filter((option) => {
150
151
  if (ids.has(option.id)) return false;
151
152
  ids.add(option.id);
152
- if (fullSet) return true;
153
+ if (fullSet || !current.filterByLabel) return true;
153
154
  return option.label.toLocaleLowerCase().includes(query.toLocaleLowerCase());
154
155
  }).map((option) => ({
155
156
  ...option,
@@ -165,7 +166,6 @@ export default function controller(host) {
165
166
  // the group-name list below. Concatenating a Map iterator produces one
166
167
  // iterator row instead of the option objects it contains.
167
168
  host.state.rows = Array.from(groups.values()).flat();
168
- host.state.loading = false;
169
169
  // When nothing matches what was typed, the offer to create it is the only choice, so it is
170
170
  // the one Enter makes, and it is highlighted as such.
171
171
  if (query && !host.state.rows.some((row) => !row.disabled) && canCreate()) host.state.active = host.state.rows.length;
@@ -267,7 +267,8 @@ export default function controller(host) {
267
267
  };
268
268
  // A strict combobox (single, no free text, no create) behaves like a select: leaving it resolves the
269
269
  // typed text to a valid option (the highlighted one, an exact label, the first label it begins, or the
270
- // only remaining option); with no match it reverts to the previous selection, or clears.
270
+ // only remaining option); with no match it reverts to the previous selection, or clears. With filter
271
+ // "none" the remaining options are all of them, whatever their labels, so the only one listed counts.
271
272
  const resolveTyped = (trigger) => {
272
273
  const current = config();
273
274
  if (host.state.multiple || current.allowFreeText || current.allowCreate) return;
@@ -285,7 +286,7 @@ export default function controller(host) {
285
286
  ?? enabled.find((row) => row.label.toLocaleLowerCase() === query)
286
287
  ?? enabled.find((row) => row.label.toLocaleLowerCase().startsWith(query))
287
288
  ?? (() => {
288
- const containing = enabled.filter((option) => option.label.toLocaleLowerCase().includes(query));
289
+ const containing = current.filterByLabel ? enabled.filter((option) => option.label.toLocaleLowerCase().includes(query)) : enabled;
289
290
  return containing.length === 1 ? containing[0] : undefined;
290
291
  })();
291
292
  if (match) commit(match.value, match.label, match, "selection", trigger);
@@ -382,8 +383,7 @@ export default function controller(host) {
382
383
  host.state.createIndex = rows.length;
383
384
  // What a named combobox submits in single mode: the value, never the label the field shows.
384
385
  host.state.submitted = String(host.state.selected ?? (host.state.allowFreeText ? host.state.raw ?? "" : ""));
385
- host.state.message = host.state.loading ? "Loading suggestions…"
386
- : host.state.lookupError || (!rows.length && !host.state.creatable ? "No suggestions." : "");
386
+ host.state.message = host.state.loading ? "Loading suggestions…" : host.state.lookupError || "";
387
387
  const validation = host.state.validation ?? { status: "pristine", issues: [] };
388
388
  host.state.validationStatus = validation.status;
389
389
  const status = host.state.loading ? "Loading suggestions…" : host.state.lookupError || (host.state.expanded ? `${rows.length} suggestions available.` : "");
@@ -452,6 +452,9 @@ export default function controller(host) {
452
452
  if (!host.state.multiple && config().allowFreeText && host.state.selected === null) commit(null, host.state.raw, null, "free-entry", "keyboard");
453
453
  void validateCurrent();
454
454
  };
455
+ const onFocusin = (event) => {
456
+ if (event.target === input && host.state.openOnFocus && !host.state.expanded) open();
457
+ };
455
458
  const onCompositionstart = (event) => { if (event.target === input) composing = true; };
456
459
  const onCompositionend = (event) => { if (event.target === input) { composing = false; onInput(new InputEvent("input")); } };
457
460
  const onTooltipOpen = (event) => { if (event.target.closest?.('[role="tooltip"]')) host.state.helpOpen = true; };
@@ -479,7 +482,7 @@ export default function controller(host) {
479
482
 
480
483
  const api = { get input() { return input; }, validate: validateCurrent };
481
484
  instances.set(element, api);
482
- const listeners = { input: onInput, keydown: onKeydown, click: onClick, pointerdown: onPointerdown, focusout: onFocusout, compositionstart: onCompositionstart, compositionend: onCompositionend, open: onTooltipOpen, close: onTooltipClose };
485
+ const listeners = { input: onInput, keydown: onKeydown, click: onClick, pointerdown: onPointerdown, focusin: onFocusin, focusout: onFocusout, compositionstart: onCompositionstart, compositionend: onCompositionend, open: onTooltipOpen, close: onTooltipClose };
483
486
  for (const [name, listener] of Object.entries(listeners)) element.addEventListener(name, listener);
484
487
  // Authored options can change after mount (renamed, replaced, or arriving late). A selected value
485
488
  // then shows its current label, unless the user is editing the text.
@@ -11,11 +11,27 @@
11
11
  name="close"
12
12
  type="object({ open: boolean, reason: action | programmatic | light-dismiss | escape, trigger: keyboard | pointer | programmatic })"
13
13
  ></event>
14
- <event name="select" type="object({ value: string, trigger: keyboard | pointer | programmatic })"></event>
14
+ <event name="select" type="object({ value: string, checked?: boolean, trigger: keyboard | pointer | programmatic })"></event>
15
15
  </defs>
16
16
  <span><div class="menu" $ref="menu" role="menu" aria-orientation="vertical"><slot></slot></div></span>
17
17
  <style>
18
18
  /* This component's hooks style the element they are set on, and never a nested one. */
19
+ @property --ui-context-menu-surface {
20
+ syntax: "*";
21
+ inherits: false;
22
+ }
23
+ @property --ui-context-menu-border {
24
+ syntax: "*";
25
+ inherits: false;
26
+ }
27
+ @property --ui-context-menu-radius {
28
+ syntax: "*";
29
+ inherits: false;
30
+ }
31
+ @property --ui-context-menu-shadow {
32
+ syntax: "*";
33
+ inherits: false;
34
+ }
19
35
  @property --ui-context-menu-z-index {
20
36
  syntax: "*";
21
37
  inherits: false;
@@ -23,6 +39,10 @@
23
39
 
24
40
  :host {
25
41
  /* Hooks do not inherit, so the parts below read them from the root. */
42
+ --_ui-context-menu-surface: var(--ui-context-menu-surface);
43
+ --_ui-context-menu-border: var(--ui-context-menu-border);
44
+ --_ui-context-menu-radius: var(--ui-context-menu-radius);
45
+ --_ui-context-menu-shadow: var(--ui-context-menu-shadow);
26
46
  --_ui-context-menu-z-index: var(--ui-context-menu-z-index);
27
47
 
28
48
  display: contents;
@@ -36,10 +56,10 @@
36
56
  flex-direction: column;
37
57
  box-sizing: border-box;
38
58
  padding: var(--ui-space-1);
39
- background: var(--ui-surface-elevated);
40
- border: 1px solid var(--ui-border-default);
41
- border-radius: var(--ui-radius-lg);
42
- box-shadow: var(--ui-elevation-overlay, var(--ui-shadow-lg));
59
+ background: var(--_ui-context-menu-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
60
+ border: 1px solid var(--_ui-context-menu-border, var(--ui-overlay-border, var(--ui-border-default)));
61
+ border-radius: var(--_ui-context-menu-radius, var(--ui-overlay-radius, var(--ui-radius-lg)));
62
+ box-shadow: var(--_ui-context-menu-shadow, var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg))));
43
63
  outline: 0;
44
64
  overflow: auto;
45
65
  z-index: var(--_ui-context-menu-z-index, 1000);
@@ -53,6 +73,14 @@
53
73
  overlay var(--ui-motion-fast) allow-discrete;
54
74
  }
55
75
 
76
+ .menu hr {
77
+ align-self: stretch;
78
+ block-size: 0;
79
+ margin: var(--ui-space-1) 0;
80
+ border: 0;
81
+ border-block-start: 1px solid var(--ui-border-default);
82
+ }
83
+
56
84
  .menu:popover-open {
57
85
  opacity: 1;
58
86
  transform: translateY(0) scale(1);
@@ -1,4 +1,5 @@
1
1
  import { closeOverlay, createAnchoredSurface, createIdResolver, openOverlay, requestTopOverlayClose } from "../shared/overlay.js";
2
+ import { menuItemFrom, menuItems, navigateMenu } from "../shared/menu-navigation.js";
2
3
 
3
4
  function disabled(item) {
4
5
  return item.getAttribute("aria-disabled") === "true" || item.hasAttribute("disabled") || item.getAttribute("disabled") === "true";
@@ -17,7 +18,7 @@ export default function controller(host) {
17
18
  let lastOpenProp = Boolean(host.state.open);
18
19
  host.state.internalOpen = lastOpenProp;
19
20
 
20
- const items = () => Array.from(element.querySelectorAll('[role="menuitem"]')).filter((item) => !disabled(item));
21
+ const items = () => menuItems(element);
21
22
  const focusFirst = () => requestAnimationFrame(() => items()[0]?.focus());
22
23
  const targetEvents = { contextmenu: onContextMenu, keydown: onTargetKeydown, pointerdown: onPointerdown, pointerup: endPress, pointercancel: endPress };
23
24
  const detach = () => {
@@ -101,33 +102,41 @@ export default function controller(host) {
101
102
  clearTimeout(press?.timer);
102
103
  press = null;
103
104
  }
105
+ const select = (item, trigger) => {
106
+ if (!item || disabled(item)) return;
107
+ const value = item.getAttribute("data-value") ?? item.getAttribute("value") ?? "";
108
+ if (item.getAttribute("role") !== "menuitem") {
109
+ const detail = { trigger, checked: undefined };
110
+ item.dispatchEvent(new CustomEvent("ui-menu-item-activate", { detail }));
111
+ host.dispatch("select", { value, checked: detail.checked, trigger });
112
+ return;
113
+ }
114
+ host.dispatch("select", { value, trigger });
115
+ close("action", trigger);
116
+ };
104
117
  const onKeydown = (event) => {
105
118
  if (!host.state.internalOpen) return;
106
119
  if (event.key === "Escape") {
107
120
  event.preventDefault();
108
121
  requestTopOverlayClose(document, "escape", "keyboard");
109
122
  } else if (["Enter", " "].includes(event.key)) {
110
- const item = event.target.closest?.('[role="menuitem"]');
123
+ const item = menuItemFrom(event.target);
111
124
  if (!item || disabled(item)) return;
112
- event.preventDefault();
113
- host.dispatch("select", { value: item.getAttribute("data-value") ?? item.getAttribute("value") ?? "", trigger: "keyboard" });
114
- close("action", "keyboard");
115
- } else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
116
- const enabled = items();
117
- if (!enabled.length) return;
118
- event.preventDefault();
119
- const index = enabled.indexOf(event.target.closest?.('[role="menuitem"]'));
120
- const next = event.key === "ArrowDown"
121
- ? (index < 0 ? 0 : Math.min(index + 1, enabled.length - 1))
122
- : (index <= 0 ? enabled.length - 1 : index - 1);
123
- enabled[next].focus();
125
+ if (item.localName === "a") {
126
+ if (event.key === " ") { event.preventDefault(); item.click(); }
127
+ } else {
128
+ event.preventDefault();
129
+ select(item, "keyboard");
130
+ }
131
+ } else {
132
+ navigateMenu(event, element);
124
133
  }
125
134
  };
126
135
  const onClick = (event) => {
127
- const item = event.target.closest?.('[role="menuitem"]');
136
+ const item = menuItemFrom(event.target);
128
137
  if (!item || disabled(item)) return;
129
- host.dispatch("select", { value: item.getAttribute("data-value") ?? item.getAttribute("value") ?? "", trigger: "pointer" });
130
- close("action", "pointer");
138
+ const trigger = event.detail === 0 ? "keyboard" : "pointer";
139
+ select(item, trigger);
131
140
  };
132
141
  resolveTargets();
133
142
  if (menuSurface) surface = createAnchoredSurface(menuSurface, { anchor: target, placement: "bottom-start" });
@@ -1,13 +1,19 @@
1
1
  <template component="ui-dialog" controller="./ui-dialog.js">
2
2
  <defs>
3
3
  <prop name="open" type="boolean" default="false">Whether the dialog is open initially or when set externally.</prop>
4
- <prop name="modal" type="boolean" default="false">Open in the top layer with a backdrop and make the rest of the page inert.</prop>
5
- <prop name="dismissible" type="boolean" default="false"
6
- >Also close on Escape and on a pointer press outside the dialog. The header close button and actions always close it.</prop>
4
+ <prop name="modeless" type="boolean" default="false">Open without a backdrop or making the rest of the page inert; dialogs are modal by default.</prop>
5
+ <prop name="closedby" type="any | closerequest | none" default="closerequest"
6
+ >Which user close requests work: any includes outside presses, closerequest includes Escape, and none requires an action.</prop>
7
+ <prop name="alert" type="boolean" default="false">Use alert dialog semantics and require an explicit action instead of the header close button.</prop>
8
+ <prop name="size" type="sm | md | lg | fullscreen" default="md">Choose a compact, standard, wide, or viewport-filling dialog.</prop>
7
9
  <prop name="label" type="string">Visible title in the dialog header; also its accessible name.</prop>
8
10
  <prop name="for" type="string" default="">ID of the control that opens the dialog.</prop>
9
11
  <state name="internalOpen" :value="false"></state>
10
12
  <state name="accessibleLabel" :value="'Dialog'"></state>
13
+ <event
14
+ name="open"
15
+ type="object({ open: boolean, reason: action | programmatic, trigger: keyboard | pointer | programmatic })"
16
+ ></event>
11
17
  <event
12
18
  name="close"
13
19
  type="object({ open: boolean, reason: action | programmatic | light-dismiss | escape, trigger: keyboard | pointer | programmatic })"
@@ -27,6 +33,22 @@
27
33
  syntax: "*";
28
34
  inherits: false;
29
35
  }
36
+ @property --ui-dialog-surface {
37
+ syntax: "*";
38
+ inherits: false;
39
+ }
40
+ @property --ui-dialog-border {
41
+ syntax: "*";
42
+ inherits: false;
43
+ }
44
+ @property --ui-dialog-radius {
45
+ syntax: "*";
46
+ inherits: false;
47
+ }
48
+ @property --ui-dialog-shadow {
49
+ syntax: "*";
50
+ inherits: false;
51
+ }
30
52
  @property --ui-dialog-max-width {
31
53
  syntax: "*";
32
54
  inherits: false;
@@ -45,7 +67,7 @@
45
67
  box-sizing: border-box;
46
68
  inline-size: min(
47
69
  calc(100% - var(--ui-dialog-viewport-gap, var(--ui-space-4)) * 2),
48
- var(--ui-dialog-max-width, 440px)
70
+ var(--ui-dialog-max-width, var(--_ui-dialog-size, 440px))
49
71
  );
50
72
  max-inline-size: none;
51
73
  max-block-size: calc(100dvh - var(--ui-dialog-viewport-gap, var(--ui-space-4)) * 2);
@@ -53,10 +75,10 @@
53
75
  padding: 0;
54
76
  overflow: hidden;
55
77
  flex-direction: column;
56
- background: var(--ui-surface-elevated);
57
- border: 1px solid var(--ui-border-default);
58
- border-radius: var(--ui-radius-dialog);
59
- box-shadow: var(--ui-elevation-dialog, var(--ui-shadow-xl));
78
+ background: var(--ui-dialog-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
79
+ border: 1px solid var(--ui-dialog-border, var(--ui-overlay-border, var(--ui-border-default)));
80
+ border-radius: var(--ui-dialog-radius, var(--ui-overlay-radius, var(--ui-radius-dialog)));
81
+ box-shadow: var(--ui-dialog-shadow, var(--ui-overlay-shadow, var(--ui-elevation-dialog, var(--ui-shadow-xl))));
60
82
  visibility: hidden;
61
83
  pointer-events: none;
62
84
  opacity: 0;
@@ -78,6 +100,27 @@
78
100
  animation: ui-dialog-enter var(--ui-dialog-duration, var(--ui-motion-base)) var(--ui-motion-ease);
79
101
  }
80
102
 
103
+ :host-state([size="sm"]) {
104
+ --_ui-dialog-size: 360px;
105
+ }
106
+
107
+ :host-state([size="lg"]) {
108
+ --_ui-dialog-size: 640px;
109
+ }
110
+
111
+ :host-state([size="fullscreen"]) {
112
+ inset: 0;
113
+ inline-size: 100%;
114
+ block-size: 100dvh;
115
+ max-block-size: none;
116
+ margin: 0;
117
+ border-radius: var(--ui-dialog-radius, 0);
118
+ }
119
+
120
+ :host-state([alert]) .close {
121
+ display: none;
122
+ }
123
+
81
124
  /* Header and actions stay pinned; only the body scrolls when content is long. */
82
125
  header {
83
126
  display: flex;
@@ -13,16 +13,21 @@ export default function controller(host) {
13
13
  let trigger = null;
14
14
  let lastFor = "";
15
15
  let lastExternalOpen = Boolean(host.state.open);
16
+ let lastOpen;
16
17
  host.state.internalOpen = lastExternalOpen;
17
18
 
18
19
  const requestClose = (reason, input) => {
19
20
  host.state.internalOpen = false;
21
+ lastOpen = false;
20
22
  host.dispatch("close", { open: false, reason, trigger: input });
21
23
  };
22
24
  const closeButton = host.refs.close;
23
25
  const onCloseClick = (event) => requestClose("action", event.detail === 0 ? "keyboard" : "pointer");
24
- const onTriggerClick = () => {
26
+ const onTriggerClick = (event) => {
27
+ if (host.state.internalOpen) return;
25
28
  host.state.internalOpen = true;
29
+ lastOpen = true;
30
+ host.dispatch("open", { open: true, reason: "action", trigger: event.detail === 0 ? "keyboard" : "pointer" });
26
31
  };
27
32
  const ids = createIdResolver(document, () => {
28
33
  lastFor = null;
@@ -49,30 +54,46 @@ export default function controller(host) {
49
54
  }
50
55
  host.state.accessibleLabel = inferredLabel(element, host.state.label);
51
56
  dialog?.setAttribute("aria-label", String(host.state.accessibleLabel));
57
+ if (host.state.alert) dialog?.setAttribute("role", "alertdialog");
58
+ else dialog?.removeAttribute("role");
52
59
  syncTrigger();
53
60
  const open = Boolean(host.state.internalOpen);
61
+ const modal = !host.state.modeless;
62
+ const requestedClosedBy = String(host.state.closedby ?? "closerequest");
63
+ const closedBy = host.state.alert && requestedClosedBy === "any" ? "closerequest" : requestedClosedBy;
54
64
  trigger?.setAttribute("aria-expanded", String(open));
55
65
  if (!dialog) return;
66
+ dialog.setAttribute("closedby", closedBy);
56
67
  if (open) {
68
+ if (dialog.open && dialog.matches(":modal") !== modal) dialog.close();
57
69
  if (!dialog.open) {
58
- if (host.state.modal) dialog.showModal();
70
+ if (modal) dialog.showModal();
59
71
  else dialog.show();
60
72
  }
61
- openOverlay({ id: overlayId, modal: Boolean(host.state.modal), element, dismissible: Boolean(host.state.dismissible), requestClose });
73
+ openOverlay({ id: overlayId, modal, element, dismissible: true, canClose: (reason) => reason === "light-dismiss" ? closedBy === "any" : reason !== "escape" || closedBy !== "none", requestClose });
62
74
  } else {
63
75
  if (dialog.open) dialog.close();
64
76
  closeOverlay(document, overlayId);
65
77
  }
78
+ if (lastOpen !== undefined && lastOpen !== open) {
79
+ host.dispatch(open ? "open" : "close", { open, reason: "programmatic", trigger: "programmatic" });
80
+ }
81
+ lastOpen = open;
66
82
  };
67
- const onClose = () => {
68
- if (!host.state.internalOpen) return;
83
+ const onClose = (event) => {
84
+ if (event instanceof CustomEvent) return;
85
+ // Consumers receive one Looma close event with a reason, not an additional native close event.
86
+ event.stopImmediatePropagation();
87
+ if (dialog.open || !host.state.internalOpen) return;
69
88
  host.state.internalOpen = false;
89
+ lastOpen = false;
70
90
  closeOverlay(document, overlayId);
71
91
  host.dispatch("close", { open: false, reason: "programmatic", trigger: "programmatic" });
72
92
  };
73
93
  const onKeydown = (event) => {
74
94
  if (event.key === "Escape") {
75
95
  event.preventDefault();
96
+ event.stopPropagation();
76
97
  requestTopOverlayClose(document, "escape", "keyboard");
77
98
  }
78
99
  };
@@ -85,7 +106,7 @@ export default function controller(host) {
85
106
  if (label !== host.state.accessibleLabel) host.state.accessibleLabel = label;
86
107
  });
87
108
  observer.observe(element, { childList: true, subtree: true, characterData: true });
88
- dialog?.addEventListener("close", onClose);
109
+ dialog?.addEventListener("close", onClose, true);
89
110
  dialog?.addEventListener("cancel", onCancel);
90
111
  closeButton?.addEventListener("click", onCloseClick);
91
112
  element.addEventListener("keydown", onKeydown);
@@ -95,7 +116,7 @@ export default function controller(host) {
95
116
  stop();
96
117
  ids.stop();
97
118
  observer.disconnect();
98
- dialog?.removeEventListener("close", onClose);
119
+ dialog?.removeEventListener("close", onClose, true);
99
120
  dialog?.removeEventListener("cancel", onCancel);
100
121
  closeButton?.removeEventListener("click", onCloseClick);
101
122
  trigger?.removeEventListener("click", onTriggerClick);
@@ -7,8 +7,15 @@
7
7
  >Whether the disclosure is open initially or when set externally. The trigger toggles it from there, reporting each change as an open or close event.</prop>
8
8
  <prop name="disabled" type="boolean" default="false"
9
9
  >Disables the trigger and mutes its text, so the disclosure keeps its current state and the user cannot toggle it.</prop>
10
+ <prop name="name" type="string" default=""
11
+ >Disclosures with the same nonempty name form an exclusive group in one document: opening one closes the others. An empty name allows multiple open
12
+ items.</prop>
13
+ <prop name="headingLevel" type="integer" default="0"
14
+ >Adds heading semantics around the trigger at level 2 through 6. Zero leaves it a plain disclosure; choose a level that matches the page outline.</prop>
10
15
  <state name="internalOpen" :value="false"></state>
11
16
  <state name="contentId" :value="''"></state>
17
+ <state name="headingRole" :value="null"></state>
18
+ <state name="headingAriaLevel" :value="null"></state>
12
19
  <event
13
20
  name="open"
14
21
  type="object({ open: boolean, reason: action | programmatic | light-dismiss | escape, trigger: keyboard | pointer | programmatic })"
@@ -19,11 +26,13 @@
19
26
  ></event>
20
27
  </defs>
21
28
  <div>
22
- <button class="trigger" type="button" $ref="trigger" :aria-controls="contentId" :aria-expanded="internalOpen" :disabled="disabled">
23
- <span $value="summary"></span>
24
- <span class="chevron" aria-hidden="true"></span>
25
- </button>
26
- <div class="panel" :id="contentId" :inert="not internalOpen"><div class="panel-inner"><slot></slot></div></div>
29
+ <div class="heading" :role="headingRole" :aria-level="headingAriaLevel">
30
+ <button class="trigger" type="button" $ref="trigger" :aria-controls="contentId" :aria-expanded="internalOpen" :disabled="disabled">
31
+ <span class="summary"><slot name="summary"><template $value="summary"></template></slot></span>
32
+ <span class="chevron" aria-hidden="true"></span>
33
+ </button>
34
+ </div>
35
+ <div class="panel" $ref="panel" :id="contentId"><div class="panel-inner"><slot></slot></div></div>
27
36
  </div>
28
37
  <style>
29
38
  /* This component's hooks style the element they are set on, and never a nested one. */