@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
@@ -1,28 +1,75 @@
1
1
  import { trackTrigger } from "../shared/trigger.js";
2
2
 
3
3
  let disclosures = 0;
4
+ const groups = new WeakMap();
5
+
6
+ function groupFor(document, name) {
7
+ let byName = groups.get(document);
8
+ if (!byName) { byName = new Map(); groups.set(document, byName); }
9
+ let members = byName.get(name);
10
+ if (!members) { members = new Set(); byName.set(name, members); }
11
+ return members;
12
+ }
4
13
 
5
14
  // `open` sets the disclosure initially and whenever it changes; the trigger toggles the state.
6
15
  export default function controller(host) {
16
+ const document = host.element.ownerDocument;
7
17
  const [trigger, stopTracking] = trackTrigger(host);
8
18
  host.state.contentId = `ui-disclosure-${++disclosures}`;
9
19
  let external = host.state.open;
10
20
  host.state.internalOpen = Boolean(external);
21
+ let groupName = "";
22
+ const member = {
23
+ close(reason, how) {
24
+ if (!host.state.internalOpen) return;
25
+ host.state.internalOpen = false;
26
+ host.dispatch("close", { open: false, reason, trigger: how });
27
+ },
28
+ };
29
+ const syncGroup = (reason, how) => {
30
+ const name = String(host.state.name || "");
31
+ if (name !== groupName) {
32
+ if (groupName) groupFor(document, groupName).delete(member);
33
+ groupName = name;
34
+ if (groupName) groupFor(document, groupName).add(member);
35
+ }
36
+ if (name && host.state.internalOpen) {
37
+ for (const peer of groupFor(document, name)) if (peer !== member) peer.close(reason, how);
38
+ }
39
+ };
11
40
  const stop = host.effect(() => {
12
- if (host.state.open === external) return;
13
- external = host.state.open;
14
- host.state.internalOpen = Boolean(external);
41
+ if (host.state.open !== external) {
42
+ external = host.state.open;
43
+ host.state.internalOpen = Boolean(external);
44
+ }
45
+ const level = Number(host.state.headingLevel);
46
+ host.state.headingRole = Number.isInteger(level) && level >= 2 && level <= 6 ? "heading" : null;
47
+ host.state.headingAriaLevel = host.state.headingRole ? level : null;
48
+ syncGroup("programmatic", "programmatic");
49
+ if (host.state.internalOpen) host.refs.panel.removeAttribute("hidden");
50
+ else host.refs.panel.setAttribute("hidden", "until-found");
15
51
  });
16
52
  const onClick = () => {
17
53
  if (host.state.disabled) return;
18
54
  const open = !host.state.internalOpen;
55
+ const how = trigger();
19
56
  host.state.internalOpen = open;
20
- host.dispatch(open ? "open" : "close", { open, reason: "action", trigger: trigger() });
57
+ if (open) syncGroup("action", how);
58
+ host.dispatch(open ? "open" : "close", { open, reason: "action", trigger: how });
21
59
  };
60
+ const onBeforematch = () => queueMicrotask(() => {
61
+ if (host.state.internalOpen) return;
62
+ host.state.internalOpen = true;
63
+ syncGroup("programmatic", "programmatic");
64
+ host.dispatch("open", { open: true, reason: "programmatic", trigger: "programmatic" });
65
+ });
22
66
  host.refs.trigger.addEventListener("click", onClick);
67
+ host.refs.panel.addEventListener("beforematch", onBeforematch);
23
68
  return () => {
24
69
  stop();
25
70
  stopTracking();
71
+ if (groupName) groupFor(document, groupName).delete(member);
26
72
  host.refs.trigger.removeEventListener("click", onClick);
73
+ host.refs.panel.removeEventListener("beforematch", onBeforematch);
27
74
  };
28
75
  }
@@ -9,6 +9,11 @@
9
9
  <prop name="hint" type="string" default="">Optional text shown beside the value to signal it is editable (for example "Edit").</prop>
10
10
  <prop name="actions" type="boolean" default="false"
11
11
  >Show explicit Save and Cancel buttons while editing. Without them, Enter or leaving the field saves and Escape cancels.</prop>
12
+ <prop name="saveLabel" type="string" default="Save">Visible text for the Save action.</prop>
13
+ <prop name="cancelLabel" type="string" default="Cancel">Visible text for the Cancel action.</prop>
14
+ <prop name="placeholder" type="string" default="">Hint shown in the text input while the draft is empty.</prop>
15
+ <prop name="required" type="boolean" default="false">Prevent an empty draft from being saved.</prop>
16
+ <prop name="maxLength" type="number" default="0">Maximum draft length; 0 leaves the length unrestricted.</prop>
12
17
  <state name="internalEdit" :value="false"></state>
13
18
  <state name="internalValue" :value="''"></state>
14
19
  <state name="draft" :value="''"></state>
@@ -25,11 +30,20 @@
25
30
  <span class="text" $value="internalValue"></span>
26
31
  <span $if="hint" class="hint" aria-hidden="true" $value="hint"></span>
27
32
  </button>
28
- <input class="input" $ref="input" type="text" .value="draft" :aria-label="label" :disabled="disabled or not internalEdit">
33
+ <input
34
+ class="input"
35
+ $ref="input"
36
+ type="text"
37
+ .value="draft"
38
+ :aria-label="label"
39
+ :placeholder="placeholder"
40
+ :required="required"
41
+ :disabled="disabled or not internalEdit"
42
+ >
29
43
  </div>
30
44
  <div $if="actions" class="actions">
31
- <button class="save" type="button">Save</button>
32
- <button class="cancel" type="button">Cancel</button>
45
+ <button class="save" type="button" $value="saveLabel"></button>
46
+ <button class="cancel" type="button" $value="cancelLabel"></button>
33
47
  </div>
34
48
  </div>
35
49
  <style>
@@ -29,6 +29,9 @@ export default function controller(host) {
29
29
  }
30
30
  // The inactive input stays in layout (it shares the value's cell) but out of the tab order.
31
31
  input.tabIndex = host.state.internalEdit ? 0 : -1;
32
+ const maxLength = Number(host.state.maxLength);
33
+ if (Number.isFinite(maxLength) && maxLength > 0) input.maxLength = Math.floor(maxLength);
34
+ else input.removeAttribute("maxlength");
32
35
  };
33
36
 
34
37
  const setEditing = (next, reason, trigger, returnFocus = true) => {
@@ -55,6 +58,10 @@ export default function controller(host) {
55
58
  const commit = (trigger, returnFocus) => {
56
59
  const previousValue = String(host.state.internalValue ?? "");
57
60
  const value = String(host.state.draft ?? "");
61
+ if (!input.checkValidity() || (input.maxLength >= 0 && value.length > input.maxLength)) {
62
+ input.reportValidity();
63
+ return;
64
+ }
58
65
  host.state.internalValue = value;
59
66
  setEditing(false, "commit", trigger, returnFocus);
60
67
  if (value !== previousValue) host.dispatch("change", { value, previousValue, trigger });
@@ -4,7 +4,11 @@
4
4
  <prop name="maxRows" type="integer" default="8">The most rows the grid offers (at most 10).</prop>
5
5
  <prop name="maxCols" type="integer" default="8">The most columns the grid offers (at most 10).</prop>
6
6
  <prop name="headerRow" type="boolean" default="false">Whether table insertion initially includes a header row.</prop>
7
- <state name="cells" type="list(object({ key: string, row: integer, col: integer, label: string, selected: boolean }))" :value="[]"></state>
7
+ <state
8
+ name="cells"
9
+ type="list(object({ key: string, row: integer, col: integer, label: string, selected: boolean, tabIndex: integer }))"
10
+ :value="[]"
11
+ ></state>
8
12
  <state name="rows" :value="8"></state>
9
13
  <state name="cols" :value="8"></state>
10
14
  <state name="hint" :value="''"></state>
@@ -13,7 +17,7 @@
13
17
  </defs>
14
18
  <div :hidden="not open">
15
19
  <p class="hint" $value="hint"></p>
16
- <div class="grid" role="group" aria-label="Table dimensions" style:--_rows="rows" style:--_cols="cols">
20
+ <div class="grid" $ref="grid" role="group" aria-label="Table dimensions" style:--_rows="rows" style:--_cols="cols">
17
21
  <button
18
22
  $each="cell of cells"
19
23
  $key="cell.key"
@@ -24,11 +28,11 @@
24
28
  :data-col="cell.col"
25
29
  :aria-label="cell.label"
26
30
  :aria-pressed="cell.selected"
31
+ :tabindex="cell.tabIndex"
27
32
  >
28
33
  </button>
29
34
  </div>
30
35
  <label class="header"><input type="checkbox" $ref="header" .checked="withHeaderRow"> Header row</label>
31
- <button type="button" class="insert" $ref="insert">Insert table</button>
32
36
  </div>
33
37
  <style>
34
38
  :host {
@@ -51,6 +55,28 @@
51
55
  grid-template-rows: repeat(var(--_rows, 8), 1.25rem);
52
56
  gap: 2px;
53
57
  margin-bottom: var(--ui-space-2);
58
+ max-inline-size: 100%;
59
+ overflow: auto;
60
+ --_ui-scroll-fade-direction: to right;
61
+ --_ui-scroll-fade-size: var(--ui-scroll-fade-size, 24px);
62
+ mask-image: linear-gradient(
63
+ var(--_ui-scroll-fade-direction),
64
+ transparent,
65
+ #000 var(--_ui-scroll-fade-start),
66
+ #000 calc(100% - var(--_ui-scroll-fade-end)),
67
+ transparent
68
+ );
69
+ animation: ui-scroll-fade-start linear, ui-scroll-fade-start-hold linear, ui-scroll-fade-end-hold linear, ui-scroll-fade-end linear;
70
+ animation-timeline: scroll(self inline);
71
+ animation-range:
72
+ 0 var(--_ui-scroll-fade-size),
73
+ var(--_ui-scroll-fade-size) calc(100% + 1px),
74
+ -1px calc(100% - var(--_ui-scroll-fade-size)),
75
+ calc(100% - var(--_ui-scroll-fade-size)) 100%;
76
+ }
77
+
78
+ :host:dir(rtl) .grid {
79
+ --_ui-scroll-fade-direction: to left;
54
80
  }
55
81
 
56
82
  .cell {
@@ -68,6 +94,11 @@
68
94
  border-color: var(--ui-accent-solid, var(--ui-accent));
69
95
  }
70
96
 
97
+ .cell:focus-visible {
98
+ outline: 2px solid var(--ui-focus-ring);
99
+ outline-offset: -2px;
100
+ }
101
+
71
102
  .header {
72
103
  display: block;
73
104
  margin-bottom: var(--ui-space-2);
@@ -75,19 +106,22 @@
75
106
  cursor: pointer;
76
107
  }
77
108
 
78
- :host .insert {
79
- width: 100%;
80
- padding: var(--ui-space-2) var(--ui-space-3);
81
- border: 1px solid var(--ui-border);
82
- border-radius: var(--ui-radius-sm);
83
- background: var(--ui-accent-solid, var(--ui-accent));
84
- color: var(--ui-accent-contrast);
85
- font-size: var(--ui-font-size-sm);
86
- cursor: pointer;
109
+ @media (pointer: coarse) {
110
+ .grid {
111
+ grid-template-columns: repeat(var(--_cols, 8), 44px);
112
+ grid-template-rows: repeat(var(--_rows, 8), 44px);
113
+ }
114
+
115
+ .cell {
116
+ width: 44px;
117
+ height: 44px;
118
+ }
87
119
  }
88
120
 
89
- :host .insert:hover {
90
- filter: brightness(1.05);
121
+ @media (forced-colors: active) {
122
+ .cell:focus-visible {
123
+ outline-color: Highlight;
124
+ }
91
125
  }
92
126
 
93
127
  :host[hidden] {
@@ -3,26 +3,26 @@ function bounded(value, fallback) {
3
3
  return Math.min(10, Math.max(1, Number.isFinite(parsed) ? parsed : fallback));
4
4
  }
5
5
 
6
- // Lays out the grid cells, previews the size under the pointer, and inserts the chosen size.
6
+ // Lays out the grid cells and inserts the chosen size by pointer or keyboard.
7
7
  export default function controller(host) {
8
8
  const element = host.element;
9
9
  let selected = { rows: 3, cols: 3 };
10
10
  let preview = null;
11
- let committed = false;
12
11
  let lastHeaderRow = Boolean(host.state.headerRow);
13
12
  host.state.withHeaderRow = lastHeaderRow;
14
13
 
15
14
  const update = () => {
16
15
  const rows = bounded(host.state.maxRows, 8);
17
16
  const cols = bounded(host.state.maxCols, 8);
17
+ selected = { rows: Math.min(selected.rows, rows), cols: Math.min(selected.cols, cols) };
18
18
  const active = preview ?? selected;
19
19
  host.state.rows = rows;
20
20
  host.state.cols = cols;
21
- host.state.hint = preview ? `${preview.rows} × ${preview.cols}` : `${selected.rows} × ${selected.cols}${committed ? " selected" : ""}`;
21
+ host.state.hint = `${active.rows} × ${active.cols}`;
22
22
  host.state.cells = Array.from({ length: rows * cols }, (_, index) => {
23
23
  const row = Math.floor(index / cols) + 1;
24
24
  const col = (index % cols) + 1;
25
- return { key: `${row}:${col}`, row, col, label: `${row} rows by ${col} columns`, selected: row <= active.rows && col <= active.cols };
25
+ return { key: `${row}:${col}`, row, col, label: `${row} rows by ${col} columns`, selected: row <= active.rows && col <= active.cols, tabIndex: row === active.rows && col === active.cols ? 0 : -1 };
26
26
  });
27
27
  };
28
28
  const stop = host.effect(() => {
@@ -42,9 +42,7 @@ export default function controller(host) {
42
42
  if (cell) {
43
43
  selected = cell;
44
44
  preview = null;
45
- committed = true;
46
45
  update();
47
- } else if (host.refs.insert.contains(event.target)) {
48
46
  host.dispatch("insert", { ...selected, withHeaderRow: host.state.withHeaderRow });
49
47
  }
50
48
  };
@@ -54,18 +52,54 @@ export default function controller(host) {
54
52
  update();
55
53
  };
56
54
  const onMouseleave = () => {
55
+ if (host.refs.grid.contains(element.ownerDocument.activeElement)) return;
57
56
  preview = null;
58
57
  update();
59
58
  };
59
+ const onFocusin = (event) => {
60
+ const cell = cellOf(event);
61
+ if (!cell) return;
62
+ preview = cell;
63
+ update();
64
+ };
65
+ const onFocusout = () => queueMicrotask(() => {
66
+ if (host.refs.grid.contains(element.ownerDocument.activeElement)) return;
67
+ preview = null;
68
+ update();
69
+ });
70
+ const onKeydown = (event) => {
71
+ const current = cellOf(event);
72
+ if (!current) return;
73
+ const rows = host.state.rows;
74
+ const cols = host.state.cols;
75
+ let next = current;
76
+ if (event.key === "ArrowRight") next = { ...current, cols: Math.min(cols, current.cols + 1) };
77
+ else if (event.key === "ArrowLeft") next = { ...current, cols: Math.max(1, current.cols - 1) };
78
+ else if (event.key === "ArrowDown") next = { ...current, rows: Math.min(rows, current.rows + 1) };
79
+ else if (event.key === "ArrowUp") next = { ...current, rows: Math.max(1, current.rows - 1) };
80
+ else if (event.key === "Home") next = { ...current, cols: 1 };
81
+ else if (event.key === "End") next = { ...current, cols };
82
+ else return;
83
+ event.preventDefault();
84
+ const cell = Array.from(host.refs.grid.querySelectorAll("[data-row][data-col]")).find((candidate) =>
85
+ Number(candidate.dataset.row) === next.rows && Number(candidate.dataset.col) === next.cols);
86
+ cell?.focus();
87
+ };
60
88
  element.addEventListener("click", onClick);
61
89
  element.addEventListener("mouseover", onMouseover);
62
90
  element.addEventListener("mouseleave", onMouseleave);
91
+ element.addEventListener("focusin", onFocusin);
92
+ element.addEventListener("focusout", onFocusout);
93
+ element.addEventListener("keydown", onKeydown);
63
94
  host.refs.header.addEventListener("change", onChange);
64
95
  return () => {
65
96
  stop();
66
97
  element.removeEventListener("click", onClick);
67
98
  element.removeEventListener("mouseover", onMouseover);
68
99
  element.removeEventListener("mouseleave", onMouseleave);
100
+ element.removeEventListener("focusin", onFocusin);
101
+ element.removeEventListener("focusout", onFocusout);
102
+ element.removeEventListener("keydown", onKeydown);
69
103
  host.refs.header.removeEventListener("change", onChange);
70
104
  };
71
105
  }
@@ -1,3 +1,4 @@
1
+ <link rel="component" href="../ui-editor-mention-menu-item/ui-editor-mention-menu-item.html">
1
2
  <template component="ui-editor-mention-menu" controller="./ui-editor-mention-menu.js">
2
3
  <defs>
3
4
  <prop name="open" type="boolean" default="false">Whether the menu is open.</prop>
@@ -7,6 +8,8 @@
7
8
  >The zero-based index of the highlighted person, capped at the last one shown, and announced through aria-activedescendant. The menu does not handle arrow
8
9
  keys itself: the editor sets this as the user moves, while hovering a person highlights them and reports a highlight event.</prop>
9
10
  <prop name="loading" type="boolean" default="false">Whether a search is in progress.</prop>
11
+ <prop name="label" type="string" default="Mentions">Accessible name of the suggestion list.</prop>
12
+ <prop name="loadingLabel" type="string" default="Searching…">Status text shown while loading an empty result list.</prop>
10
13
  <prop
11
14
  name="anchorRect"
12
15
  type="object({ left?: number, top?: number, right?: number, bottom?: number, x?: number, y?: number, width?: number, height?: number }) | null"
@@ -16,15 +19,12 @@
16
19
  <state name="searching" :value="false"></state>
17
20
  <state name="active" :value="0"></state>
18
21
  <state name="prefix" :value="'ui-editor-mention-menu'"></state>
19
- <event name="highlight" type="object({ index: integer })"></event>
20
- <event name="select" type="object({ index: integer })"></event>
22
+ <event name="highlight" type="object({ index: integer, value: string })"></event>
23
+ <event name="select" type="object({ index: integer, value: string })"></event>
21
24
  </defs>
22
- <div role="listbox" aria-label="Mention a person" :aria-busy="loading" :hidden="not visible">
23
- <div class="header">
24
- <span>People</span>
25
- <span class="hint">Type after @ to search</span>
26
- </div>
27
- <div $if="searching" class="loading" role="status">Searching…</div>
25
+ <div role="listbox" :aria-busy="loading" :hidden="not visible">
26
+ <div class="header"><slot name="header"></slot></div>
27
+ <div $if="searching" class="loading" role="status" $value="loadingLabel"></div>
28
28
  <ul $if="not searching" class="list" role="presentation">
29
29
  <li
30
30
  $each="row, index of rows"
@@ -34,11 +34,14 @@
34
34
  role="option"
35
35
  :aria-selected="index = active"
36
36
  :data-index="index"
37
+ :data-value="row.id"
37
38
  >
38
39
  <span class="avatar" aria-hidden="true" $value="row.initials"></span>
39
40
  <span class="body"><span class="label" $value="row.label"></span><span $if="row.detail" class="detail" $value="row.detail"></span></span>
40
41
  </li>
42
+ <slot></slot>
41
43
  </ul>
44
+ <div class="empty" $ref="empty"><slot name="empty"></slot></div>
42
45
  </div>
43
46
  <style>
44
47
  :host {
@@ -67,6 +70,16 @@
67
70
  font-weight: var(--ui-font-semibold);
68
71
  }
69
72
 
73
+ .header:not(:has(> *)), .empty:not(:has(> *)) {
74
+ display: none;
75
+ }
76
+
77
+ .empty {
78
+ padding: var(--ui-space-3);
79
+ color: var(--ui-text-muted);
80
+ font-size: var(--ui-font-size-sm);
81
+ }
82
+
70
83
  .hint {
71
84
  overflow: hidden;
72
85
  color: var(--ui-text-muted);
@@ -123,7 +136,7 @@
123
136
  flex: 0 0 auto;
124
137
  color: var(--ui-accent-active, var(--ui-accent-solid));
125
138
  background: var(--ui-accent-soft, var(--ui-accent-subtle));
126
- border-radius: 999px;
139
+ border-radius: var(--ui-radius-round);
127
140
  font-size: var(--ui-font-size-xs);
128
141
  font-weight: var(--ui-font-semibold);
129
142
  }
@@ -3,33 +3,54 @@ import { normalizeAnchor, positionMenu } from "../shared/editor.js";
3
3
  // Places the menu at the @ and derives its rows; the template renders them.
4
4
  export default function controller(host) {
5
5
  const element = host.element;
6
+ const authoredLabel = element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby");
6
7
  const place = () => {
7
8
  if (host.state.visible) positionMenu(element, normalizeAnchor(host.state.anchorRect), 320);
8
9
  };
10
+ const options = () => Array.from(element.querySelectorAll('[role="option"]'));
11
+ const syncOptions = () => {
12
+ const rows = options();
13
+ const active = Math.min(Math.max(0, host.state.active), Math.max(0, rows.length - 1));
14
+ rows.forEach((row, index) => {
15
+ row.dataset.index = String(index);
16
+ row.id = `${host.state.prefix}-option-${index}`;
17
+ row.setAttribute("aria-selected", String(index === active));
18
+ });
19
+ if (rows.length) element.setAttribute("aria-activedescendant", rows[active].id);
20
+ else element.removeAttribute("aria-activedescendant");
21
+ const empty = Boolean(host.refs.empty.children.length);
22
+ host.refs.empty.hidden = rows.length > 0 || host.state.loading;
23
+ const visible = Boolean(host.state.open && normalizeAnchor(host.state.anchorRect)
24
+ && (host.state.loading || rows.length > 0 || empty));
25
+ if (host.state.visible !== visible) host.state.visible = visible;
26
+ place();
27
+ };
9
28
  const stop = host.effect(() => {
29
+ if (!authoredLabel) element.setAttribute("aria-label", String(host.state.label || "Mentions"));
10
30
  const items = (Array.isArray(host.state.items) ? host.state.items : []).slice(0, 20);
11
31
  host.state.rows = items.map((item) => ({ ...item, initials: item.initials ?? String(item.label).slice(0, 2).toUpperCase() }));
12
32
  host.state.searching = Boolean(host.state.loading && !items.length);
13
33
  host.state.active = Math.min(Number(host.state.selectedIndex ?? 0), Math.max(0, items.length - 1));
14
34
  host.state.prefix = element.id || "ui-editor-mention-menu";
15
- host.state.visible = Boolean(host.state.open && (host.state.loading || items.length > 0) && normalizeAnchor(host.state.anchorRect));
16
- if (items.length) element.setAttribute("aria-activedescendant", `${host.state.prefix}-option-${host.state.active}`);
17
- else element.removeAttribute("aria-activedescendant");
18
- place();
35
+ queueMicrotask(syncOptions);
19
36
  });
20
- const indexOf = (event) => Number(event.target.closest?.("[data-index]")?.dataset.index);
37
+ const rowOf = (event) => event.target.closest?.('[role="option"]');
21
38
  const onMousedown = (event) => event.preventDefault();
22
39
  const onClick = (event) => {
23
- const index = indexOf(event);
24
- if (Number.isFinite(index)) host.dispatch("select", { index });
40
+ const row = rowOf(event);
41
+ if (row) host.dispatch("select", { index: Number(row.dataset.index), value: row.dataset.value ?? "" });
25
42
  };
26
43
  const onMouseover = (event) => {
27
- const index = indexOf(event);
28
- if (!Number.isFinite(index) || index === host.state.active) return;
44
+ const row = rowOf(event);
45
+ if (!row) return;
46
+ const index = Number(row.dataset.index);
47
+ if (index === host.state.active) return;
29
48
  host.state.active = index;
30
- element.setAttribute("aria-activedescendant", `${host.state.prefix}-option-${index}`);
31
- host.dispatch("highlight", { index });
49
+ syncOptions();
50
+ host.dispatch("highlight", { index, value: row.dataset.value ?? "" });
32
51
  };
52
+ const observer = new MutationObserver(syncOptions);
53
+ observer.observe(element, { childList: true, subtree: true });
33
54
  element.addEventListener("mousedown", onMousedown);
34
55
  element.addEventListener("click", onClick);
35
56
  element.addEventListener("mouseover", onMouseover);
@@ -39,6 +60,7 @@ export default function controller(host) {
39
60
  window.visualViewport?.addEventListener("scroll", place);
40
61
  return () => {
41
62
  stop();
63
+ observer.disconnect();
42
64
  element.removeEventListener("mousedown", onMousedown);
43
65
  element.removeEventListener("click", onClick);
44
66
  element.removeEventListener("mouseover", onMouseover);
@@ -0,0 +1,67 @@
1
+ <template component="ui-editor-mention-menu-item">
2
+ <defs>
3
+ <prop name="value" type="string" default="">Stable identifier reported by the containing Mention Menu when this person is chosen.</prop>
4
+ <prop name="detail" type="string" default="">Secondary text, such as a team or role.</prop>
5
+ <prop name="initials" type="string" default="">Text fallback for the start slot when no avatar is supplied.</prop>
6
+ </defs>
7
+ <li role="option" tabindex="-1" :data-value="value">
8
+ <span class="avatar" aria-hidden="true"><slot name="start"><template $value="initials"></template></slot></span>
9
+ <span class="body"><span class="label"><slot></slot></span><span $if="detail" class="detail" $value="detail"></span></span>
10
+ </li>
11
+ <style>
12
+ :host {
13
+ display: flex;
14
+ align-items: center;
15
+ min-block-size: 44px;
16
+ padding: var(--ui-space-2) var(--ui-space-3);
17
+ gap: var(--ui-space-3);
18
+ cursor: pointer;
19
+ list-style: none;
20
+ }
21
+
22
+ :host[aria-selected="true"] {
23
+ background: var(--ui-accent-subtle);
24
+ }
25
+
26
+ .avatar {
27
+ display: inline-flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ inline-size: 32px;
31
+ block-size: 32px;
32
+ flex: 0 0 auto;
33
+ color: var(--ui-accent-active);
34
+ background: var(--ui-accent-soft);
35
+ border-radius: var(--ui-radius-round);
36
+ font-size: var(--ui-font-size-xs);
37
+ font-weight: var(--ui-font-semibold);
38
+ }
39
+
40
+ .body {
41
+ display: flex;
42
+ min-inline-size: 0;
43
+ flex-direction: column;
44
+ }
45
+
46
+ .label, .detail {
47
+ overflow: hidden;
48
+ text-overflow: ellipsis;
49
+ white-space: nowrap;
50
+ }
51
+
52
+ .label {
53
+ color: var(--ui-text-primary);
54
+ font-size: var(--ui-font-size-sm);
55
+ font-weight: var(--ui-font-medium);
56
+ }
57
+
58
+ .detail {
59
+ color: var(--ui-text-muted);
60
+ font-size: var(--ui-font-size-xs);
61
+ }
62
+
63
+ :host[hidden] {
64
+ display: none;
65
+ }
66
+ </style>
67
+ </template>