@nldd/design-system 0.8.43 → 0.8.45

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 (459) hide show
  1. package/dist/components/actions/button/button.d.ts +19 -1
  2. package/dist/components/actions/button/button.d.ts.map +1 -1
  3. package/dist/components/actions/button/button.js +25 -1
  4. package/dist/components/actions/button/button.js.map +1 -1
  5. package/dist/components/actions/button/button.styles.d.ts.map +1 -1
  6. package/dist/components/actions/button/button.styles.js +177 -299
  7. package/dist/components/actions/button/button.styles.js.map +1 -1
  8. package/dist/components/actions/button/button.template.d.ts.map +1 -1
  9. package/dist/components/actions/button/button.template.js +7 -0
  10. package/dist/components/actions/button/button.template.js.map +1 -1
  11. package/dist/components/actions/button-bar/button-bar.styles.d.ts.map +1 -1
  12. package/dist/components/actions/button-bar/button-bar.styles.js +33 -79
  13. package/dist/components/actions/button-bar/button-bar.styles.js.map +1 -1
  14. package/dist/components/actions/button-group/button-group.styles.d.ts.map +1 -1
  15. package/dist/components/actions/button-group/button-group.styles.js +24 -37
  16. package/dist/components/actions/button-group/button-group.styles.js.map +1 -1
  17. package/dist/components/actions/icon-button/icon-button.d.ts +28 -7
  18. package/dist/components/actions/icon-button/icon-button.d.ts.map +1 -1
  19. package/dist/components/actions/icon-button/icon-button.js +36 -9
  20. package/dist/components/actions/icon-button/icon-button.js.map +1 -1
  21. package/dist/components/actions/icon-button/icon-button.styles.d.ts.map +1 -1
  22. package/dist/components/actions/icon-button/icon-button.styles.js +173 -299
  23. package/dist/components/actions/icon-button/icon-button.styles.js.map +1 -1
  24. package/dist/components/actions/icon-button/icon-button.template.d.ts.map +1 -1
  25. package/dist/components/actions/icon-button/icon-button.template.js +9 -2
  26. package/dist/components/actions/icon-button/icon-button.template.js.map +1 -1
  27. package/dist/components/actions/split-button/split-button.d.ts +5 -5
  28. package/dist/components/actions/split-button/split-button.d.ts.map +1 -1
  29. package/dist/components/actions/split-button/split-button.js +19 -15
  30. package/dist/components/actions/split-button/split-button.js.map +1 -1
  31. package/dist/components/actions/split-button/split-button.styles.d.ts.map +1 -1
  32. package/dist/components/actions/split-button/split-button.styles.js +32 -67
  33. package/dist/components/actions/split-button/split-button.styles.js.map +1 -1
  34. package/dist/components/actions/split-button/split-button.template.d.ts.map +1 -1
  35. package/dist/components/actions/split-button/split-button.template.js +14 -12
  36. package/dist/components/actions/split-button/split-button.template.js.map +1 -1
  37. package/dist/components/actions/toolbar/toolbar.d.ts +0 -2
  38. package/dist/components/actions/toolbar/toolbar.d.ts.map +1 -1
  39. package/dist/components/actions/toolbar/toolbar.js +21 -17
  40. package/dist/components/actions/toolbar/toolbar.js.map +1 -1
  41. package/dist/components/actions/toolbar/toolbar.styles.d.ts.map +1 -1
  42. package/dist/components/actions/toolbar/toolbar.styles.js +52 -105
  43. package/dist/components/actions/toolbar/toolbar.styles.js.map +1 -1
  44. package/dist/components/actions/toolbar/toolbar.template.d.ts +1 -1
  45. package/dist/components/actions/toolbar/toolbar.template.d.ts.map +1 -1
  46. package/dist/components/actions/toolbar/toolbar.template.js +13 -5
  47. package/dist/components/actions/toolbar/toolbar.template.js.map +1 -1
  48. package/dist/components/components.js +1 -1
  49. package/dist/components/content/blockquote/blockquote.d.ts +1 -9
  50. package/dist/components/content/blockquote/blockquote.d.ts.map +1 -1
  51. package/dist/components/content/blockquote/blockquote.js +1 -6
  52. package/dist/components/content/blockquote/blockquote.js.map +1 -1
  53. package/dist/components/content/blockquote/blockquote.styles.d.ts.map +1 -1
  54. package/dist/components/content/blockquote/blockquote.styles.js +16 -15
  55. package/dist/components/content/blockquote/blockquote.styles.js.map +1 -1
  56. package/dist/components/content/blockquote/blockquote.template.d.ts.map +1 -1
  57. package/dist/components/content/blockquote/blockquote.template.js +4 -7
  58. package/dist/components/content/blockquote/blockquote.template.js.map +1 -1
  59. package/dist/components/content/code/code.d.ts +4 -0
  60. package/dist/components/content/code/code.d.ts.map +1 -1
  61. package/dist/components/content/code/code.js +20 -0
  62. package/dist/components/content/code/code.js.map +1 -1
  63. package/dist/components/content/code/code.styles.d.ts.map +1 -1
  64. package/dist/components/content/code/code.styles.js +10 -26
  65. package/dist/components/content/code/code.styles.js.map +1 -1
  66. package/dist/components/content/icon/icon-aliases.d.ts.map +1 -1
  67. package/dist/components/content/icon/icon-aliases.js +31 -7
  68. package/dist/components/content/icon/icon-aliases.js.map +1 -1
  69. package/dist/components/content/icon/icon-registry.d.ts.map +1 -1
  70. package/dist/components/content/icon/icon-registry.js +18 -6
  71. package/dist/components/content/icon/icon-registry.js.map +1 -1
  72. package/dist/components/content/icon/icon.d.ts +26 -1
  73. package/dist/components/content/icon/icon.d.ts.map +1 -1
  74. package/dist/components/content/icon/icon.js +30 -1
  75. package/dist/components/content/icon/icon.js.map +1 -1
  76. package/dist/components/content/icon/icon.styles.d.ts.map +1 -1
  77. package/dist/components/content/icon/icon.styles.js +65 -14
  78. package/dist/components/content/icon/icon.styles.js.map +1 -1
  79. package/dist/components/content/icon/icon.template.d.ts.map +1 -1
  80. package/dist/components/content/icon/icon.template.js +1 -5
  81. package/dist/components/content/icon/icon.template.js.map +1 -1
  82. package/dist/components/content/keyboard-shortcut/keyboard-shortcut.d.ts +22 -0
  83. package/dist/components/content/keyboard-shortcut/keyboard-shortcut.d.ts.map +1 -1
  84. package/dist/components/content/keyboard-shortcut/keyboard-shortcut.js +46 -2
  85. package/dist/components/content/keyboard-shortcut/keyboard-shortcut.js.map +1 -1
  86. package/dist/components/content/keyboard-shortcut/keyboard-shortcut.styles.d.ts.map +1 -1
  87. package/dist/components/content/keyboard-shortcut/keyboard-shortcut.styles.js +12 -18
  88. package/dist/components/content/keyboard-shortcut/keyboard-shortcut.styles.js.map +1 -1
  89. package/dist/components/content/rich-text/rich-text.d.ts +3 -1
  90. package/dist/components/content/rich-text/rich-text.d.ts.map +1 -1
  91. package/dist/components/content/rich-text/rich-text.js +6 -1
  92. package/dist/components/content/rich-text/rich-text.js.map +1 -1
  93. package/dist/components/content/tag/tag.d.ts +5 -0
  94. package/dist/components/content/tag/tag.d.ts.map +1 -1
  95. package/dist/components/content/tag/tag.js +22 -0
  96. package/dist/components/content/tag/tag.js.map +1 -1
  97. package/dist/components/content/tag/tag.styles.d.ts.map +1 -1
  98. package/dist/components/content/tag/tag.styles.js +16 -27
  99. package/dist/components/content/tag/tag.styles.js.map +1 -1
  100. package/dist/components/content/tag/tag.template.d.ts.map +1 -1
  101. package/dist/components/content/tag/tag.template.js +12 -8
  102. package/dist/components/content/tag/tag.template.js.map +1 -1
  103. package/dist/components/content/title/title.styles.d.ts.map +1 -1
  104. package/dist/components/content/title/title.styles.js +84 -97
  105. package/dist/components/content/title/title.styles.js.map +1 -1
  106. package/dist/components/content/tooltip/tooltip.styles.d.ts.map +1 -1
  107. package/dist/components/content/tooltip/tooltip.styles.js +19 -22
  108. package/dist/components/content/tooltip/tooltip.styles.js.map +1 -1
  109. package/dist/components/forms/form-actions/form-actions.styles.d.ts.map +1 -1
  110. package/dist/components/forms/form-actions/form-actions.styles.js +18 -18
  111. package/dist/components/forms/form-actions/form-actions.styles.js.map +1 -1
  112. package/dist/components/forms/form-field/form-field.styles.d.ts.map +1 -1
  113. package/dist/components/forms/form-field/form-field.styles.js +44 -53
  114. package/dist/components/forms/form-field/form-field.styles.js.map +1 -1
  115. package/dist/components/{index-By2jYHEw.js → index-iTa3Ua3y.js} +5021 -6751
  116. package/dist/components/index.d.ts +1 -1
  117. package/dist/components/index.d.ts.map +1 -1
  118. package/dist/components/index.js +1 -1
  119. package/dist/components/index.js.map +1 -1
  120. package/dist/components/inputs/checkbox/checkbox.styles.d.ts.map +1 -1
  121. package/dist/components/inputs/checkbox/checkbox.styles.js +12 -33
  122. package/dist/components/inputs/checkbox/checkbox.styles.js.map +1 -1
  123. package/dist/components/inputs/checkbox-field/checkbox-field.styles.d.ts.map +1 -1
  124. package/dist/components/inputs/checkbox-field/checkbox-field.styles.js +7 -10
  125. package/dist/components/inputs/checkbox-field/checkbox-field.styles.js.map +1 -1
  126. package/dist/components/inputs/code-editor/code-editor.styles.d.ts.map +1 -1
  127. package/dist/components/inputs/code-editor/code-editor.styles.js +26 -28
  128. package/dist/components/inputs/code-editor/code-editor.styles.js.map +1 -1
  129. package/dist/components/inputs/combo-box/combo-box.d.ts +2 -0
  130. package/dist/components/inputs/combo-box/combo-box.d.ts.map +1 -1
  131. package/dist/components/inputs/combo-box/combo-box.js +5 -0
  132. package/dist/components/inputs/combo-box/combo-box.js.map +1 -1
  133. package/dist/components/inputs/combo-box/combo-box.styles.d.ts.map +1 -1
  134. package/dist/components/inputs/combo-box/combo-box.styles.js +43 -70
  135. package/dist/components/inputs/combo-box/combo-box.styles.js.map +1 -1
  136. package/dist/components/inputs/combo-box/combo-box.template.d.ts.map +1 -1
  137. package/dist/components/inputs/combo-box/combo-box.template.js +3 -2
  138. package/dist/components/inputs/combo-box/combo-box.template.js.map +1 -1
  139. package/dist/components/inputs/dropdown/dropdown.d.ts +15 -5
  140. package/dist/components/inputs/dropdown/dropdown.d.ts.map +1 -1
  141. package/dist/components/inputs/dropdown/dropdown.js +28 -9
  142. package/dist/components/inputs/dropdown/dropdown.js.map +1 -1
  143. package/dist/components/inputs/dropdown/dropdown.styles.d.ts.map +1 -1
  144. package/dist/components/inputs/dropdown/dropdown.styles.js +98 -107
  145. package/dist/components/inputs/dropdown/dropdown.styles.js.map +1 -1
  146. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.d.ts +2 -0
  147. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.d.ts.map +1 -1
  148. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.js +5 -0
  149. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.js.map +1 -1
  150. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.styles.d.ts.map +1 -1
  151. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.styles.js +40 -66
  152. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.styles.js.map +1 -1
  153. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.template.d.ts.map +1 -1
  154. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.template.js +1 -0
  155. package/dist/components/inputs/multi-line-text-field/multi-line-text-field.template.js.map +1 -1
  156. package/dist/components/inputs/number-field/number-field.styles.d.ts.map +1 -1
  157. package/dist/components/inputs/number-field/number-field.styles.js +37 -61
  158. package/dist/components/inputs/number-field/number-field.styles.js.map +1 -1
  159. package/dist/components/inputs/number-field/number-field.template.js +2 -2
  160. package/dist/components/inputs/password-field/password-field.d.ts +8 -8
  161. package/dist/components/inputs/password-field/password-field.d.ts.map +1 -1
  162. package/dist/components/inputs/password-field/password-field.js +16 -16
  163. package/dist/components/inputs/password-field/password-field.js.map +1 -1
  164. package/dist/components/inputs/password-field/password-field.styles.d.ts.map +1 -1
  165. package/dist/components/inputs/password-field/password-field.styles.js +55 -101
  166. package/dist/components/inputs/password-field/password-field.styles.js.map +1 -1
  167. package/dist/components/inputs/password-field/password-field.template.d.ts.map +1 -1
  168. package/dist/components/inputs/password-field/password-field.template.js +4 -3
  169. package/dist/components/inputs/password-field/password-field.template.js.map +1 -1
  170. package/dist/components/inputs/radio-button/radio-button.styles.d.ts.map +1 -1
  171. package/dist/components/inputs/radio-button/radio-button.styles.js +14 -35
  172. package/dist/components/inputs/radio-button/radio-button.styles.js.map +1 -1
  173. package/dist/components/inputs/radio-button-field/radio-button-field.styles.d.ts.map +1 -1
  174. package/dist/components/inputs/radio-button-field/radio-button-field.styles.js +7 -10
  175. package/dist/components/inputs/radio-button-field/radio-button-field.styles.js.map +1 -1
  176. package/dist/components/inputs/search-field/search-field.d.ts +2 -0
  177. package/dist/components/inputs/search-field/search-field.d.ts.map +1 -1
  178. package/dist/components/inputs/search-field/search-field.js +5 -0
  179. package/dist/components/inputs/search-field/search-field.js.map +1 -1
  180. package/dist/components/inputs/search-field/search-field.styles.d.ts.map +1 -1
  181. package/dist/components/inputs/search-field/search-field.styles.js +49 -83
  182. package/dist/components/inputs/search-field/search-field.styles.js.map +1 -1
  183. package/dist/components/inputs/search-field/search-field.template.d.ts.map +1 -1
  184. package/dist/components/inputs/search-field/search-field.template.js +1 -0
  185. package/dist/components/inputs/search-field/search-field.template.js.map +1 -1
  186. package/dist/components/inputs/segmented-control/segmented-control.styles.d.ts.map +1 -1
  187. package/dist/components/inputs/segmented-control/segmented-control.styles.js +64 -115
  188. package/dist/components/inputs/segmented-control/segmented-control.styles.js.map +1 -1
  189. package/dist/components/inputs/segmented-control/segmented-control.template.js +1 -1
  190. package/dist/components/inputs/segmented-control/segmented-control.template.js.map +1 -1
  191. package/dist/components/inputs/stepper/stepper.styles.d.ts.map +1 -1
  192. package/dist/components/inputs/stepper/stepper.styles.js +18 -32
  193. package/dist/components/inputs/stepper/stepper.styles.js.map +1 -1
  194. package/dist/components/inputs/stepper/stepper.template.js +2 -2
  195. package/dist/components/inputs/switch/switch.styles.d.ts.map +1 -1
  196. package/dist/components/inputs/switch/switch.styles.js +37 -80
  197. package/dist/components/inputs/switch/switch.styles.js.map +1 -1
  198. package/dist/components/inputs/switch-field/switch-field.styles.d.ts.map +1 -1
  199. package/dist/components/inputs/switch-field/switch-field.styles.js +7 -10
  200. package/dist/components/inputs/switch-field/switch-field.styles.js.map +1 -1
  201. package/dist/components/inputs/text-field/text-field.d.ts +2 -0
  202. package/dist/components/inputs/text-field/text-field.d.ts.map +1 -1
  203. package/dist/components/inputs/text-field/text-field.js +5 -0
  204. package/dist/components/inputs/text-field/text-field.js.map +1 -1
  205. package/dist/components/inputs/text-field/text-field.styles.d.ts.map +1 -1
  206. package/dist/components/inputs/text-field/text-field.styles.js +42 -78
  207. package/dist/components/inputs/text-field/text-field.styles.js.map +1 -1
  208. package/dist/components/inputs/text-field/text-field.template.d.ts.map +1 -1
  209. package/dist/components/inputs/text-field/text-field.template.js +1 -0
  210. package/dist/components/inputs/text-field/text-field.template.js.map +1 -1
  211. package/dist/components/inputs/toggle-button/toggle-button.d.ts +5 -1
  212. package/dist/components/inputs/toggle-button/toggle-button.d.ts.map +1 -1
  213. package/dist/components/inputs/toggle-button/toggle-button.js +16 -2
  214. package/dist/components/inputs/toggle-button/toggle-button.js.map +1 -1
  215. package/dist/components/inputs/toggle-button/toggle-button.styles.d.ts.map +1 -1
  216. package/dist/components/inputs/toggle-button/toggle-button.styles.js +55 -102
  217. package/dist/components/inputs/toggle-button/toggle-button.styles.js.map +1 -1
  218. package/dist/components/inputs/toggle-button/toggle-button.template.d.ts.map +1 -1
  219. package/dist/components/inputs/toggle-button/toggle-button.template.js +10 -4
  220. package/dist/components/inputs/toggle-button/toggle-button.template.js.map +1 -1
  221. package/dist/components/inputs/token/token.styles.d.ts.map +1 -1
  222. package/dist/components/inputs/token/token.styles.js +23 -49
  223. package/dist/components/inputs/token/token.styles.js.map +1 -1
  224. package/dist/components/inputs/token/token.template.js +1 -1
  225. package/dist/components/layout/app-view/app-view.styles.d.ts.map +1 -1
  226. package/dist/components/layout/app-view/app-view.styles.js +15 -9
  227. package/dist/components/layout/app-view/app-view.styles.js.map +1 -1
  228. package/dist/components/layout/box/box.styles.js +2 -2
  229. package/dist/components/layout/card/card.styles.d.ts.map +1 -1
  230. package/dist/components/layout/card/card.styles.js +5 -11
  231. package/dist/components/layout/card/card.styles.js.map +1 -1
  232. package/dist/components/layout/collection/collection.styles.d.ts.map +1 -1
  233. package/dist/components/layout/collection/collection.styles.js +23 -45
  234. package/dist/components/layout/collection/collection.styles.js.map +1 -1
  235. package/dist/components/layout/collection/collection.template.d.ts.map +1 -1
  236. package/dist/components/layout/collection/collection.template.js +1 -0
  237. package/dist/components/layout/collection/collection.template.js.map +1 -1
  238. package/dist/components/layout/container/container.d.ts +41 -43
  239. package/dist/components/layout/container/container.d.ts.map +1 -1
  240. package/dist/components/layout/container/container.js +143 -132
  241. package/dist/components/layout/container/container.js.map +1 -1
  242. package/dist/components/layout/container/container.styles.d.ts.map +1 -1
  243. package/dist/components/layout/container/container.styles.js +87 -1133
  244. package/dist/components/layout/container/container.styles.js.map +1 -1
  245. package/dist/components/layout/container/container.template.d.ts.map +1 -1
  246. package/dist/components/layout/container/container.template.js +1 -5
  247. package/dist/components/layout/container/container.template.js.map +1 -1
  248. package/dist/components/layout/divider/divider.styles.d.ts.map +1 -1
  249. package/dist/components/layout/divider/divider.styles.js +5 -3
  250. package/dist/components/layout/divider/divider.styles.js.map +1 -1
  251. package/dist/components/layout/page/page.styles.d.ts.map +1 -1
  252. package/dist/components/layout/page/page.styles.js +21 -27
  253. package/dist/components/layout/page/page.styles.js.map +1 -1
  254. package/dist/components/layout/page-sections/full-bleed-section/full-bleed-section.styles.d.ts.map +1 -1
  255. package/dist/components/layout/page-sections/full-bleed-section/full-bleed-section.styles.js +16 -24
  256. package/dist/components/layout/page-sections/full-bleed-section/full-bleed-section.styles.js.map +1 -1
  257. package/dist/components/layout/page-sections/one-half-one-half-section/one-half-one-half-section.styles.d.ts.map +1 -1
  258. package/dist/components/layout/page-sections/one-half-one-half-section/one-half-one-half-section.styles.js +15 -14
  259. package/dist/components/layout/page-sections/one-half-one-half-section/one-half-one-half-section.styles.js.map +1 -1
  260. package/dist/components/layout/page-sections/one-third-two-thirds-section/one-third-two-thirds-section.styles.d.ts.map +1 -1
  261. package/dist/components/layout/page-sections/one-third-two-thirds-section/one-third-two-thirds-section.styles.js +15 -14
  262. package/dist/components/layout/page-sections/one-third-two-thirds-section/one-third-two-thirds-section.styles.js.map +1 -1
  263. package/dist/components/layout/page-sections/simple-section/simple-section.styles.d.ts.map +1 -1
  264. package/dist/components/layout/page-sections/simple-section/simple-section.styles.js +15 -24
  265. package/dist/components/layout/page-sections/simple-section/simple-section.styles.js.map +1 -1
  266. package/dist/components/layout/page-sections/two-thirds-one-third-section/two-thirds-one-third-section.styles.d.ts.map +1 -1
  267. package/dist/components/layout/page-sections/two-thirds-one-third-section/two-thirds-one-third-section.styles.js +15 -14
  268. package/dist/components/layout/page-sections/two-thirds-one-third-section/two-thirds-one-third-section.styles.js.map +1 -1
  269. package/dist/components/layout/popover/popover.d.ts +2 -1
  270. package/dist/components/layout/popover/popover.d.ts.map +1 -1
  271. package/dist/components/layout/popover/popover.js +21 -4
  272. package/dist/components/layout/popover/popover.js.map +1 -1
  273. package/dist/components/layout/popover/popover.styles.d.ts.map +1 -1
  274. package/dist/components/layout/popover/popover.styles.js +10 -15
  275. package/dist/components/layout/popover/popover.styles.js.map +1 -1
  276. package/dist/components/layout/sheet/sheet.d.ts +17 -4
  277. package/dist/components/layout/sheet/sheet.d.ts.map +1 -1
  278. package/dist/components/layout/sheet/sheet.js +40 -6
  279. package/dist/components/layout/sheet/sheet.js.map +1 -1
  280. package/dist/components/layout/sheet/sheet.styles.d.ts.map +1 -1
  281. package/dist/components/layout/sheet/sheet.styles.js +41 -59
  282. package/dist/components/layout/sheet/sheet.styles.js.map +1 -1
  283. package/dist/components/layout/spacer/spacer.styles.d.ts.map +1 -1
  284. package/dist/components/layout/spacer/spacer.styles.js +13 -5
  285. package/dist/components/layout/spacer/spacer.styles.js.map +1 -1
  286. package/dist/components/layout/split-views/bar-split-view/bar-split-view.styles.d.ts.map +1 -1
  287. package/dist/components/layout/split-views/bar-split-view/bar-split-view.styles.js +29 -26
  288. package/dist/components/layout/split-views/bar-split-view/bar-split-view.styles.js.map +1 -1
  289. package/dist/components/layout/split-views/navigation-split-view/navigation-split-view.styles.d.ts.map +1 -1
  290. package/dist/components/layout/split-views/navigation-split-view/navigation-split-view.styles.js +92 -105
  291. package/dist/components/layout/split-views/navigation-split-view/navigation-split-view.styles.js.map +1 -1
  292. package/dist/components/layout/split-views/side-by-side-split-view/side-by-side-split-view.styles.d.ts.map +1 -1
  293. package/dist/components/layout/split-views/side-by-side-split-view/side-by-side-split-view.styles.js +22 -15
  294. package/dist/components/layout/split-views/side-by-side-split-view/side-by-side-split-view.styles.js.map +1 -1
  295. package/dist/components/layout/split-views/split-view-divider/split-view-divider.styles.d.ts.map +1 -1
  296. package/dist/components/layout/split-views/split-view-divider/split-view-divider.styles.js +26 -46
  297. package/dist/components/layout/split-views/split-view-divider/split-view-divider.styles.js.map +1 -1
  298. package/dist/components/layout/split-views/split-view-pane/split-view-pane.styles.d.ts.map +1 -1
  299. package/dist/components/layout/split-views/split-view-pane/split-view-pane.styles.js +22 -8
  300. package/dist/components/layout/split-views/split-view-pane/split-view-pane.styles.js.map +1 -1
  301. package/dist/components/layout/split-views/stacked-split-view/stacked-split-view.styles.d.ts.map +1 -1
  302. package/dist/components/layout/split-views/stacked-split-view/stacked-split-view.styles.js +23 -16
  303. package/dist/components/layout/split-views/stacked-split-view/stacked-split-view.styles.js.map +1 -1
  304. package/dist/components/layout/window/window.styles.d.ts.map +1 -1
  305. package/dist/components/layout/window/window.styles.js +23 -31
  306. package/dist/components/layout/window/window.styles.js.map +1 -1
  307. package/dist/components/lists-and-menus/cells/cell/cell.styles.d.ts.map +1 -1
  308. package/dist/components/lists-and-menus/cells/cell/cell.styles.js +16 -15
  309. package/dist/components/lists-and-menus/cells/cell/cell.styles.js.map +1 -1
  310. package/dist/components/lists-and-menus/cells/description-cell/description-cell.styles.d.ts.map +1 -1
  311. package/dist/components/lists-and-menus/cells/description-cell/description-cell.styles.js +28 -31
  312. package/dist/components/lists-and-menus/cells/description-cell/description-cell.styles.js.map +1 -1
  313. package/dist/components/lists-and-menus/cells/drag-handle-cell/drag-handle-cell.styles.d.ts.map +1 -1
  314. package/dist/components/lists-and-menus/cells/drag-handle-cell/drag-handle-cell.styles.js +22 -28
  315. package/dist/components/lists-and-menus/cells/drag-handle-cell/drag-handle-cell.styles.js.map +1 -1
  316. package/dist/components/lists-and-menus/cells/icon-cell/icon-cell.styles.d.ts.map +1 -1
  317. package/dist/components/lists-and-menus/cells/icon-cell/icon-cell.styles.js +40 -92
  318. package/dist/components/lists-and-menus/cells/icon-cell/icon-cell.styles.js.map +1 -1
  319. package/dist/components/lists-and-menus/cells/spacer-cell/spacer-cell.styles.d.ts.map +1 -1
  320. package/dist/components/lists-and-menus/cells/spacer-cell/spacer-cell.styles.js +26 -25
  321. package/dist/components/lists-and-menus/cells/spacer-cell/spacer-cell.styles.js.map +1 -1
  322. package/dist/components/lists-and-menus/cells/text-cell/text-cell.styles.d.ts.map +1 -1
  323. package/dist/components/lists-and-menus/cells/text-cell/text-cell.styles.js +68 -129
  324. package/dist/components/lists-and-menus/cells/text-cell/text-cell.styles.js.map +1 -1
  325. package/dist/components/lists-and-menus/cells/title-cell/title-cell.styles.d.ts.map +1 -1
  326. package/dist/components/lists-and-menus/cells/title-cell/title-cell.styles.js +75 -136
  327. package/dist/components/lists-and-menus/cells/title-cell/title-cell.styles.js.map +1 -1
  328. package/dist/components/lists-and-menus/list/list.styles.d.ts.map +1 -1
  329. package/dist/components/lists-and-menus/list/list.styles.js +31 -58
  330. package/dist/components/lists-and-menus/list/list.styles.js.map +1 -1
  331. package/dist/components/lists-and-menus/list-item/list-item.styles.d.ts.map +1 -1
  332. package/dist/components/lists-and-menus/list-item/list-item.styles.js +46 -37
  333. package/dist/components/lists-and-menus/list-item/list-item.styles.js.map +1 -1
  334. package/dist/components/lists-and-menus/menu/menu.d.ts +184 -34
  335. package/dist/components/lists-and-menus/menu/menu.d.ts.map +1 -1
  336. package/dist/components/lists-and-menus/menu/menu.i18n.d.ts +2 -0
  337. package/dist/components/lists-and-menus/menu/menu.i18n.d.ts.map +1 -1
  338. package/dist/components/lists-and-menus/menu/menu.i18n.js +2 -0
  339. package/dist/components/lists-and-menus/menu/menu.i18n.js.map +1 -1
  340. package/dist/components/lists-and-menus/menu/menu.js +633 -122
  341. package/dist/components/lists-and-menus/menu/menu.js.map +1 -1
  342. package/dist/components/lists-and-menus/menu/menu.styles.d.ts.map +1 -1
  343. package/dist/components/lists-and-menus/menu/menu.styles.js +133 -119
  344. package/dist/components/lists-and-menus/menu/menu.styles.js.map +1 -1
  345. package/dist/components/lists-and-menus/menu/menu.template.d.ts.map +1 -1
  346. package/dist/components/lists-and-menus/menu/menu.template.js +15 -2
  347. package/dist/components/lists-and-menus/menu/menu.template.js.map +1 -1
  348. package/dist/components/navigation/document-tab-bar/document-tab-bar.d.ts +0 -2
  349. package/dist/components/navigation/document-tab-bar/document-tab-bar.d.ts.map +1 -1
  350. package/dist/components/navigation/document-tab-bar/document-tab-bar.js +20 -22
  351. package/dist/components/navigation/document-tab-bar/document-tab-bar.js.map +1 -1
  352. package/dist/components/navigation/document-tab-bar/document-tab-bar.styles.d.ts.map +1 -1
  353. package/dist/components/navigation/document-tab-bar/document-tab-bar.styles.js +57 -53
  354. package/dist/components/navigation/document-tab-bar/document-tab-bar.styles.js.map +1 -1
  355. package/dist/components/navigation/document-tab-bar/document-tab-bar.template.d.ts.map +1 -1
  356. package/dist/components/navigation/document-tab-bar/document-tab-bar.template.js +11 -9
  357. package/dist/components/navigation/document-tab-bar/document-tab-bar.template.js.map +1 -1
  358. package/dist/components/navigation/link/link.d.ts.map +1 -0
  359. package/dist/components/navigation/link/link.js.map +1 -0
  360. package/dist/components/navigation/link/link.styles.d.ts.map +1 -0
  361. package/dist/components/{actions → navigation}/link/link.styles.js +26 -35
  362. package/dist/components/navigation/link/link.styles.js.map +1 -0
  363. package/dist/components/navigation/link/link.template.d.ts.map +1 -0
  364. package/dist/components/navigation/link/link.template.js.map +1 -0
  365. package/dist/components/navigation/menu-bar/menu-bar.d.ts +17 -5
  366. package/dist/components/navigation/menu-bar/menu-bar.d.ts.map +1 -1
  367. package/dist/components/navigation/menu-bar/menu-bar.js +101 -37
  368. package/dist/components/navigation/menu-bar/menu-bar.js.map +1 -1
  369. package/dist/components/navigation/menu-bar/menu-bar.styles.d.ts.map +1 -1
  370. package/dist/components/navigation/menu-bar/menu-bar.styles.js +5 -4
  371. package/dist/components/navigation/menu-bar/menu-bar.styles.js.map +1 -1
  372. package/dist/components/navigation/menu-bar/menu-bar.template.d.ts.map +1 -1
  373. package/dist/components/navigation/menu-bar/menu-bar.template.js +13 -2
  374. package/dist/components/navigation/menu-bar/menu-bar.template.js.map +1 -1
  375. package/dist/components/navigation/menu-bar-item/menu-bar-item.d.ts +18 -1
  376. package/dist/components/navigation/menu-bar-item/menu-bar-item.d.ts.map +1 -1
  377. package/dist/components/navigation/menu-bar-item/menu-bar-item.js +35 -5
  378. package/dist/components/navigation/menu-bar-item/menu-bar-item.js.map +1 -1
  379. package/dist/components/navigation/menu-bar-item/menu-bar-item.styles.d.ts.map +1 -1
  380. package/dist/components/navigation/menu-bar-item/menu-bar-item.styles.js +25 -27
  381. package/dist/components/navigation/menu-bar-item/menu-bar-item.styles.js.map +1 -1
  382. package/dist/components/navigation/menu-bar-item/menu-bar-item.template.d.ts.map +1 -1
  383. package/dist/components/navigation/menu-bar-item/menu-bar-item.template.js +2 -0
  384. package/dist/components/navigation/menu-bar-item/menu-bar-item.template.js.map +1 -1
  385. package/dist/components/navigation/pagination/pagination.d.ts +12 -0
  386. package/dist/components/navigation/pagination/pagination.d.ts.map +1 -1
  387. package/dist/components/navigation/pagination/pagination.js +19 -0
  388. package/dist/components/navigation/pagination/pagination.js.map +1 -1
  389. package/dist/components/navigation/pagination/pagination.styles.d.ts.map +1 -1
  390. package/dist/components/navigation/pagination/pagination.styles.js +65 -96
  391. package/dist/components/navigation/pagination/pagination.styles.js.map +1 -1
  392. package/dist/components/navigation/pagination/pagination.template.d.ts.map +1 -1
  393. package/dist/components/navigation/pagination/pagination.template.js +23 -16
  394. package/dist/components/navigation/pagination/pagination.template.js.map +1 -1
  395. package/dist/components/navigation/skip-link/skip-link.styles.d.ts.map +1 -1
  396. package/dist/components/navigation/skip-link/skip-link.styles.js +13 -15
  397. package/dist/components/navigation/skip-link/skip-link.styles.js.map +1 -1
  398. package/dist/components/navigation/tab-bar/tab-bar.d.ts +3 -10
  399. package/dist/components/navigation/tab-bar/tab-bar.d.ts.map +1 -1
  400. package/dist/components/navigation/tab-bar/tab-bar.js +3 -28
  401. package/dist/components/navigation/tab-bar/tab-bar.js.map +1 -1
  402. package/dist/components/navigation/tab-bar/tab-bar.styles.d.ts.map +1 -1
  403. package/dist/components/navigation/tab-bar/tab-bar.styles.js +44 -110
  404. package/dist/components/navigation/tab-bar/tab-bar.styles.js.map +1 -1
  405. package/dist/components/navigation/tab-bar/tab-bar.template.js +1 -1
  406. package/dist/components/navigation/tab-bar/tab-bar.template.js.map +1 -1
  407. package/dist/components/navigation/top-navigation-bar/top-navigation-bar.d.ts +9 -1
  408. package/dist/components/navigation/top-navigation-bar/top-navigation-bar.d.ts.map +1 -1
  409. package/dist/components/navigation/top-navigation-bar/top-navigation-bar.js +56 -17
  410. package/dist/components/navigation/top-navigation-bar/top-navigation-bar.js.map +1 -1
  411. package/dist/components/navigation/top-navigation-bar/top-navigation-bar.styles.d.ts.map +1 -1
  412. package/dist/components/navigation/top-navigation-bar/top-navigation-bar.styles.js +68 -75
  413. package/dist/components/navigation/top-navigation-bar/top-navigation-bar.styles.js.map +1 -1
  414. package/dist/components/navigation/top-navigation-bar/top-navigation-bar.template.d.ts.map +1 -1
  415. package/dist/components/navigation/top-navigation-bar/top-navigation-bar.template.js +6 -10
  416. package/dist/components/navigation/top-navigation-bar/top-navigation-bar.template.js.map +1 -1
  417. package/dist/components/navigation/top-title-bar/top-title-bar.styles.d.ts.map +1 -1
  418. package/dist/components/navigation/top-title-bar/top-title-bar.styles.js +27 -48
  419. package/dist/components/navigation/top-title-bar/top-title-bar.styles.js.map +1 -1
  420. package/dist/components/{prism-typescript-BlIir5CV.js → prism-typescript-DG_p4Wyh.js} +1 -1
  421. package/dist/components/status-and-feedback/badge/badge.styles.d.ts.map +1 -1
  422. package/dist/components/status-and-feedback/badge/badge.styles.js +27 -40
  423. package/dist/components/status-and-feedback/badge/badge.styles.js.map +1 -1
  424. package/dist/components/status-and-feedback/inline-dialog/inline-dialog.styles.d.ts.map +1 -1
  425. package/dist/components/status-and-feedback/inline-dialog/inline-dialog.styles.js +37 -38
  426. package/dist/components/status-and-feedback/inline-dialog/inline-dialog.styles.js.map +1 -1
  427. package/dist/components/status-and-feedback/modal-dialog/modal-dialog.styles.d.ts.map +1 -1
  428. package/dist/components/status-and-feedback/modal-dialog/modal-dialog.styles.js +22 -26
  429. package/dist/components/status-and-feedback/modal-dialog/modal-dialog.styles.js.map +1 -1
  430. package/dist/css/rich-text.css +85 -58
  431. package/dist/css/settings.css +69 -36
  432. package/dist/utilities/color-scheme-repaint.d.ts +44 -0
  433. package/dist/utilities/color-scheme-repaint.d.ts.map +1 -0
  434. package/dist/utilities/color-scheme-repaint.js +88 -0
  435. package/dist/utilities/color-scheme-repaint.js.map +1 -0
  436. package/dist/utilities/os.d.ts +27 -0
  437. package/dist/utilities/os.d.ts.map +1 -0
  438. package/dist/utilities/os.js +60 -0
  439. package/dist/utilities/os.js.map +1 -0
  440. package/package.json +15 -12
  441. package/dist/components/actions/link/link.d.ts.map +0 -1
  442. package/dist/components/actions/link/link.js.map +0 -1
  443. package/dist/components/actions/link/link.styles.d.ts.map +0 -1
  444. package/dist/components/actions/link/link.styles.js.map +0 -1
  445. package/dist/components/actions/link/link.template.d.ts.map +0 -1
  446. package/dist/components/actions/link/link.template.js.map +0 -1
  447. package/dist/components/content/blockquote/blockquote.i18n.d.ts +0 -5
  448. package/dist/components/content/blockquote/blockquote.i18n.d.ts.map +0 -1
  449. package/dist/components/content/blockquote/blockquote.i18n.js +0 -4
  450. package/dist/components/content/blockquote/blockquote.i18n.js.map +0 -1
  451. package/dist/utilities/popover-guard.d.ts +0 -10
  452. package/dist/utilities/popover-guard.d.ts.map +0 -1
  453. package/dist/utilities/popover-guard.js +0 -10
  454. package/dist/utilities/popover-guard.js.map +0 -1
  455. /package/dist/components/{actions → navigation}/link/link.d.ts +0 -0
  456. /package/dist/components/{actions → navigation}/link/link.js +0 -0
  457. /package/dist/components/{actions → navigation}/link/link.styles.d.ts +0 -0
  458. /package/dist/components/{actions → navigation}/link/link.template.d.ts +0 -0
  459. /package/dist/components/{actions → navigation}/link/link.template.js +0 -0
@@ -15,8 +15,7 @@ import '../../lists-and-menus/cells/spacer-cell/spacer-cell.js';
15
15
  import '../../lists-and-menus/cells/text-cell/text-cell.js';
16
16
  import '../../content/icon/icon.js';
17
17
  import '../../status-and-feedback/inline-dialog/inline-dialog.js';
18
- import { isKeyboardMode } from '../../../utilities/input-modality.js';
19
- import { POPOVER_REOPEN_GUARD_MS } from '../../../utilities/popover-guard.js';
18
+ import { isKeyboardMode, isTouchMode } from '../../../utilities/input-modality.js';
20
19
  import { breakpoints } from '../../../assets/styles/breakpoints.js';
21
20
  // # nldd-menu-divider
22
21
  export class NLDDMenuDivider extends LitElement {
@@ -83,6 +82,11 @@ if (!customElements.get('nldd-menu-group')) {
83
82
  * @attr {string} icon - Icon name rendered before the text (nldd-icon name).
84
83
  * @attr {string} type - Item type: 'button' | 'checkbox' | 'radio'. Default: 'button'.
85
84
  * @attr {boolean} selected - Selected state for checkbox and radio types.
85
+ * @attr {boolean} destructive - Marks the item as destructive (red text; red highlight bg). Use for irreversible
86
+ * actions like "Delete". Colour is the only built-in signal, so per WCAG 1.4.1 the
87
+ * item's own label must convey the destructive nature (e.g. "Verwijder") — don't rely
88
+ * on the red alone. Confirming the action (e.g. a follow-up dialog) is the consumer's
89
+ * responsibility.
86
90
  * @attr {boolean} disabled - Disabled state.
87
91
  * @attr {string} query - Query substring to bold-highlight in text. Set by menu's filter(); also settable by consumers.
88
92
  * @attr {string} query-mark-mode - 'match' | 'predictive' (default: 'predictive'). See text-cell for details.
@@ -101,6 +105,7 @@ export class NLDDMenuItem extends LitElement {
101
105
  this.icon = '';
102
106
  this.type = 'button';
103
107
  this.selected = false;
108
+ this.destructive = false;
104
109
  this.disabled = false;
105
110
  this.query = '';
106
111
  this.queryMarkMode = 'predictive';
@@ -183,12 +188,35 @@ export class NLDDMenuItem extends LitElement {
183
188
  this._submenuMutationObserver?.disconnect();
184
189
  this._submenuMutationObserver = null;
185
190
  }
186
- _handleClick() {
191
+ _handleClick(event) {
187
192
  if (this.disabled)
188
193
  return;
189
194
  // Submenu items don't fire `select` — they open their submenu instead.
190
195
  // Item is either an action OR a submenu opener, not both.
191
196
  if (this._hasSubmenu) {
197
+ // The button is wired to the submenu via the popoverTargetElement
198
+ // IDL property (set in the template). That marks it as the
199
+ // submenu's invoker so the browser excludes the opener from
200
+ // popover light-dismiss — clicking the opener no longer closes
201
+ // the submenu underneath it. The flip side: the browser would
202
+ // also auto-show the popover with its default positioning,
203
+ // which races our anchor + placement set in `_handleSubmenuOpen`.
204
+ // preventDefault stops the browser action and lets our flow
205
+ // own positioning.
206
+ //
207
+ // We use the IDL property rather than the `popovertarget`
208
+ // attribute because the button lives in this menu-item's shadow
209
+ // root while the submenu is a light-DOM child — attribute-based
210
+ // ID resolution is tree-scoped and would silently fail to find
211
+ // the cross-tree target. The IDL property takes a direct element
212
+ // reference and works across shadow boundaries.
213
+ event?.preventDefault();
214
+ // Click on an already-open opener: keep the submenu visible and
215
+ // the opener visually "active" (aria-expanded + :hover). A second
216
+ // click should not toggle it closed — that conflicts with the
217
+ // hover-open semantics of cascade mode.
218
+ if (this._submenuOpen)
219
+ return;
192
220
  this.dispatchEvent(new CustomEvent('submenu-open', {
193
221
  detail: { submenu: this._submenuEl, item: this },
194
222
  bubbles: true,
@@ -233,6 +261,9 @@ __decorate([
233
261
  __decorate([
234
262
  property({ type: Boolean, reflect: true })
235
263
  ], NLDDMenuItem.prototype, "selected", void 0);
264
+ __decorate([
265
+ property({ type: Boolean, reflect: true })
266
+ ], NLDDMenuItem.prototype, "destructive", void 0);
236
267
  __decorate([
237
268
  property({ type: Boolean, reflect: true })
238
269
  ], NLDDMenuItem.prototype, "disabled", void 0);
@@ -274,9 +305,9 @@ const defaultFilterFn = (query, item) => {
274
305
  * @attr {string} empty-text - Text of the default empty-state dialog. Falls back
275
306
  * to Dutch i18n "Geen opties beschikbaar".
276
307
  * @attr {string} empty-supporting-text - Supporting text of the default empty-state dialog.
277
- * @attr {string} width - Explicit width. Sets --_menu-width internally.
308
+ * @attr {string} width - Explicit width. Sets --_width internally.
278
309
  * @attr {number} max-items - Maximum number of visible items before scrolling.
279
- * Sets --_menu-max-items internally. Default: 0 (no limit).
310
+ * Sets --_max-items internally. Default: 0 (no limit).
280
311
  * @attr {object} translations - Override one or more translation keys.
281
312
  * @attr {Function} filterFn - Custom filter function (query, item) => boolean.
282
313
  *
@@ -299,11 +330,11 @@ export class NLDDMenu extends LitElement {
299
330
  this.variant = 'menu';
300
331
  this.emptyText = '';
301
332
  this.emptySupportingText = '';
302
- /** Explicit width. Sets --_menu-width internally. */
333
+ /** Explicit width. Sets --_width internally. */
303
334
  this.width = '';
304
335
  /**
305
336
  * Maximum number of visible items before the menu scrolls.
306
- * Sets --_menu-max-items internally. Default: 0 (no limit).
337
+ * Sets --_max-items internally. Default: 0 (no limit).
307
338
  */
308
339
  this.maxItems = 0;
309
340
  /**
@@ -331,6 +362,30 @@ export class NLDDMenu extends LitElement {
331
362
  * by one of this menu's items). null when no submenu is open. */
332
363
  this._activeSubmenu = null;
333
364
  this._activeSubmenuOpener = null;
365
+ /** Cleanup closure for the currently open child submenu. Invoked by the
366
+ * submenu's `toggle→closed` event, but also callable directly from a
367
+ * chain-close walk so the upper levels can be torn down without waiting
368
+ * for stale toggle events that will never fire (e.g. a hidden drill-in
369
+ * parent whose popover state is already 'closed'). */
370
+ this._activeSubmenuCleanup = null;
371
+ /** Signal that this menu is hiding itself to make room for a deeper drill-in
372
+ * level — its parent's onToggle should skip the normal close cleanup
373
+ * (re-parent + re-show) so the chain stays in deeper-level state. */
374
+ this._drillInHidingForDeeper = false;
375
+ /** Root-owned registry of every submenu currently open in this chain,
376
+ * in open order. Lives on the root menu only; submenus reach it via
377
+ * `_chainRoot`. Drives `_collapseChain()` so collapsing the whole
378
+ * stack never depends on `_activeSubmenu`/`_parentMenu` threading
379
+ * (which real interaction can leave stale). @internal */
380
+ this._openChain = [];
381
+ /** Back-reference to the chain's root, set on every submenu when it
382
+ * opens. Lets `_collapseChain()` find the registry without walking
383
+ * the (possibly stale) parent links. null on the root. @internal */
384
+ this._chainRoot = null;
385
+ /** Force-teardown for this submenu (detach toggle listener, hide,
386
+ * restore DOM, reset state — no parent re-show). Set when the submenu
387
+ * opens; called by `_collapseChain()`. @internal */
388
+ this._collapseSelf = null;
334
389
  /** When this menu is itself a submenu, points to the parent menu that
335
390
  * opened it. Set by the parent's _handleSubmenuOpen. null on the root.
336
391
  * @internal */
@@ -340,22 +395,66 @@ export class NLDDMenu extends LitElement {
340
395
  * @internal */
341
396
  this._parentItem = null;
342
397
  this._isOpen = false;
343
- this._closedAt = 0;
398
+ /** Set on the root when a pointer gesture (outside/anchor pointerdown
399
+ * or confirmed tap) collapsed the chain, or when a pointerdown landed
400
+ * on the anchor while the menu was open (covers native light-dismiss).
401
+ * The trailing `click` of that same gesture consumes it in
402
+ * `_handleDocumentClick` and must NOT reopen — gesture-correlated, so
403
+ * it's reliable on touch regardless of pointerup→click timing.
404
+ * @internal */
405
+ this._collapsedByPointerGesture = false;
344
406
  this._cleanupAutoUpdate = null;
407
+ // — Status messages (WCAG 4.1.3) ———————————————————————————————————————————
408
+ //
409
+ // Drill-in mode swaps the whole view (parent hides, submenu shows, or
410
+ // vice versa). Focus moves to the new view's first item / opener, so AT
411
+ // announces *an* element — but not that the view itself changed. The
412
+ // polite live region in the shadow DOM (see menu.template) carries that
413
+ // explicit status. Cascade mode keeps both views visible: nothing is
414
+ // swapped, so it never announces.
415
+ /** Double-rAF handle for the announcement awaiting flush, so a rapid
416
+ * follow-up transition can cancel the stale one instead of stacking it. */
417
+ this._announceRaf = 0;
418
+ /** Message currently queued (not yet flushed to the region). Used to
419
+ * drop a doubled event in the same tick while still allowing the same
420
+ * text to be re-announced on a later, genuine transition. */
421
+ this._pendingAnnouncement = '';
345
422
  // — Event handlers ————————————————————————————————————————————————————————
346
423
  this._handleDocumentClick = (event) => {
347
424
  if (this.anchorElement)
348
425
  return;
426
+ // Consume the same-gesture collapse marker (set when an outside or
427
+ // anchor pointer gesture collapsed the chain). Read+clear for every
428
+ // click so it never leaks past a single gesture.
429
+ const collapsedByGesture = this._collapsedByPointerGesture;
430
+ this._collapsedByPointerGesture = false;
349
431
  const anchorEl = this._getAnchorEl();
350
432
  if (!anchorEl)
351
433
  return;
352
434
  const path = event.composedPath();
353
435
  if (!path.includes(anchorEl))
354
436
  return;
437
+ // This click is the tail of the very gesture that just collapsed the
438
+ // chain (e.g. tapping the anchor from deep in a drill-in submenu:
439
+ // the submenu's pointerdown/tap already collapsed). It must not
440
+ // reopen. Gesture-correlated, so reliable even when the
441
+ // pointerup→click gap exceeds the time-based reopen guard on touch.
442
+ if (collapsedByGesture)
443
+ return;
444
+ // Drill-in: the root is hidden while a submenu shows, so `_isOpen`
445
+ // is false on the root even though the menu is visibly open. A
446
+ // plain toggle would `showPopover()` the root — i.e. bounce back
447
+ // to the main level. The anchor is a toggle: if anything in the
448
+ // chain is open, clicking it collapses the whole stack (registry-
449
+ // driven, so it works even if chain links went stale).
450
+ if (this._drillInMode && (this._isOpen || this._openChain.length > 0)) {
451
+ this._collapseChain();
452
+ return;
453
+ }
355
454
  if (this._isOpen) {
356
455
  this.hidePopover();
357
456
  }
358
- else if (Date.now() - this._closedAt > POPOVER_REOPEN_GUARD_MS) {
457
+ else {
359
458
  this.showPopover();
360
459
  }
361
460
  };
@@ -369,10 +468,13 @@ export class NLDDMenu extends LitElement {
369
468
  // right edge if floating-ui flipped it to the other side). The wedge
370
469
  // stays fixed until the cursor enters the submenu, so the user has a
371
470
  // predictable, stable area to traverse without losing the submenu.
372
- // While the cursor is inside the triangle, peer items don't activate
373
- // and the linger-close timer is held off.
471
+ // While the cursor is inside the triangle, peer items don't activate.
472
+ //
473
+ // We deliberately do NOT auto-close on hover-out: moving the cursor to
474
+ // empty space outside every menu leaves the chain open. The native
475
+ // popover light-dismiss takes care of click-outside, which is the only
476
+ // gesture that should collapse the chain.
374
477
  this._hoverOpenTimer = null;
375
- this._hoverCloseTimer = null;
376
478
  this._safeTriangleListener = null;
377
479
  this._safeTriangleStallTimer = null;
378
480
  this._lastCursorPos = null;
@@ -380,6 +482,10 @@ export class NLDDMenu extends LitElement {
380
482
  this._safeTriangleSubmenu = null;
381
483
  this._movingTowardSubmenu = false;
382
484
  this._handleMenuItemMouseenter = (event) => {
485
+ // Touch suppress: synthesised mouseenter from touch (e.g. lifting
486
+ // after a scroll gesture on a long menu) shouldn't paint a highlight.
487
+ if (isTouchMode())
488
+ return;
383
489
  const item = event.target.closest('nldd-menu-item');
384
490
  if (!item)
385
491
  return;
@@ -389,8 +495,9 @@ export class NLDDMenu extends LitElement {
389
495
  if (this.variant !== 'listbox')
390
496
  this._clearHighlight();
391
497
  this._cancelHoverOpen();
392
- // The hover-guard takes over close behaviour while the cursor is
393
- // outside both the parent menu and the active submenu.
498
+ // No auto-close on hover-out: leaving the menu rect leaves the chain
499
+ // open. Click-outside (popover light-dismiss) is the only gesture
500
+ // that collapses the chain.
394
501
  };
395
502
  // — Debug overlay ————————————————————————————————————————————————————
396
503
  this._safeTriangleOverlay = null;
@@ -433,6 +540,13 @@ export class NLDDMenu extends LitElement {
433
540
  }
434
541
  submenu._parentMenu = this;
435
542
  submenu._parentItem = item;
543
+ // Register this submenu in the root's open-chain so `_collapseChain()`
544
+ // can tear the whole stack down without relying on parent links being
545
+ // intact at collapse time. Captured here, while links are fresh.
546
+ const chainRoot = this._rootMenu;
547
+ submenu._chainRoot = chainRoot;
548
+ if (!chainRoot._openChain.includes(submenu))
549
+ chainRoot._openChain.push(submenu);
436
550
  if (this._drillInMode) {
437
551
  // Drill-in: anchor to the root's anchor so all submenus open at the
438
552
  // same screen position — visually stacks. Inherit root placement
@@ -456,18 +570,58 @@ export class NLDDMenu extends LitElement {
456
570
  item.removeAttribute('highlighted');
457
571
  // Listen once for the submenu's close so we can clear state, ARIA and
458
572
  // any leftover safe-triangle plumbing from the cascade open path.
459
- const onToggle = (e) => {
460
- const tg = e;
461
- if (tg.newState !== 'closed')
462
- return;
573
+ const wasDrillIn = this._drillInMode;
574
+ // Shared teardown — runs from `toggle→closed` in the normal flow, and
575
+ // invoked directly from a select-chain-close walk for hidden ancestors
576
+ // whose toggle event will never fire (they were already closed when
577
+ // the deeper level took over the screen). `skipReshow` lets the chain
578
+ // walk suppress the parent-restore step so the whole stack collapses.
579
+ const cleanup = (skipReshow) => {
463
580
  submenu.removeEventListener('toggle', onToggle);
464
581
  if (this._activeSubmenu === submenu) {
465
582
  this._activeSubmenu = null;
466
583
  this._activeSubmenuOpener = null;
584
+ this._activeSubmenuCleanup = null;
467
585
  }
468
586
  submenu._parentMenu = null;
469
587
  submenu._parentItem = null;
588
+ // Deregister from the root's open-chain registry.
589
+ const ci = chainRoot._openChain.indexOf(submenu);
590
+ if (ci !== -1)
591
+ chainRoot._openChain.splice(ci, 1);
592
+ submenu._collapseSelf = null;
593
+ submenu._chainRoot = null;
470
594
  item._submenuOpen = false;
595
+ // Drill-in: restore the submenu to its original DOM position and
596
+ // re-show the parent so the user returns to the previous view.
597
+ // Captured at open-time so a viewport flip during navigation
598
+ // doesn't leave the chain in an inconsistent state.
599
+ if (wasDrillIn) {
600
+ if (drillInOriginalParent && drillInOriginalParent.isConnected) {
601
+ drillInOriginalParent.insertBefore(submenu, drillInNextSibling);
602
+ }
603
+ else {
604
+ // Original parent was removed from the DOM while the
605
+ // submenu was reparented to <body> (SPA navigation,
606
+ // story teardown, etc.). There's nowhere to restore it
607
+ // to — drop the orphaned node instead of leaking a
608
+ // detached-but-connected element in <body> forever.
609
+ submenu.remove();
610
+ }
611
+ if (!skipReshow && this.isConnected && !this.matches(':popover-open')) {
612
+ this.showPopover?.();
613
+ }
614
+ // One level back (back button / ArrowLeft / Esc) — announce the
615
+ // destination view on the now-visible parent. `skipReshow` is a
616
+ // collapse-the-whole-chain teardown (select / outside-tap /
617
+ // resize): that's a dismiss, not a back-nav, so it stays silent.
618
+ if (!skipReshow && this.isConnected) {
619
+ const dest = this._parentItem?.text;
620
+ this._announce(dest
621
+ ? this._t('components.menu.submenu-back-action', { title: dest })
622
+ : this._t('components.menu.back'));
623
+ }
624
+ }
471
625
  // Drop any safe-triangle "in transit" highlight on the opener —
472
626
  // without this, an opener whose submenu was closed via stall-
473
627
  // dismissal or programmatic hidePopover keeps the bold accent
@@ -480,7 +634,6 @@ export class NLDDMenu extends LitElement {
480
634
  if (!item.hasAttribute('data-focused')) {
481
635
  item.removeAttribute('highlighted');
482
636
  }
483
- this._cancelHoverClose();
484
637
  this._stopSafeTriangle();
485
638
  // Counterpart to `submenu-open` — consumers tracking submenu state
486
639
  // via declarative event listeners (rather than reaching into our
@@ -491,12 +644,59 @@ export class NLDDMenu extends LitElement {
491
644
  composed: false,
492
645
  }));
493
646
  };
647
+ const onToggle = (e) => {
648
+ const tg = e;
649
+ if (tg.newState !== 'closed')
650
+ return;
651
+ // Submenu is hiding to make room for a deeper drill — leave the
652
+ // chain state alone, keep the listener around for the eventual
653
+ // real close.
654
+ if (submenu._drillInHidingForDeeper) {
655
+ submenu._drillInHidingForDeeper = false;
656
+ return;
657
+ }
658
+ // A real toggle-close = one level back: tear down and re-show
659
+ // the parent. Collapsing the whole chain doesn't come through
660
+ // here — `_collapseChain()` drives `_collapseSelf` directly.
661
+ cleanup(false);
662
+ };
494
663
  submenu.addEventListener('toggle', onToggle);
664
+ this._activeSubmenuCleanup = cleanup;
665
+ // Force-teardown used by `_collapseChain()`: detach the toggle
666
+ // listener (so this hide doesn't also run the back-nav onToggle),
667
+ // hide, then run the shared cleanup with no parent re-show.
668
+ submenu._collapseSelf = () => {
669
+ submenu.removeEventListener('toggle', onToggle);
670
+ submenu.hidePopover?.();
671
+ cleanup(true);
672
+ };
673
+ // Drill-in: re-parent the submenu to <body> before show so it isn't
674
+ // a popover-stack descendant of the parent — that lets us close the
675
+ // parent without cascading the submenu closed. Original placement is
676
+ // restored when the submenu closes.
677
+ let drillInOriginalParent = null;
678
+ let drillInNextSibling = null;
679
+ if (wasDrillIn) {
680
+ drillInOriginalParent = submenu.parentElement;
681
+ drillInNextSibling = submenu.nextSibling;
682
+ document.body.appendChild(submenu);
683
+ }
495
684
  submenu.showPopover?.();
496
- // Start the hover triangle tracker for cascade-mode submenus only.
685
+ if (wasDrillIn) {
686
+ // View just swapped to the submenu — announce the level we
687
+ // entered (the opener's label is this view's title, same string
688
+ // the back button shows). Fires on the now-visible submenu.
689
+ submenu._announce(submenu._t('components.menu.submenu-title', { title: item.text }));
690
+ // Signal to our own parent's onToggle (if any) that this hide is
691
+ // "intentional, making room for a deeper drill" — skip the normal
692
+ // close cleanup that would re-parent us back and re-open them.
693
+ this._drillInHidingForDeeper = true;
694
+ this.hidePopover?.();
695
+ }
696
+ // Start the safe-triangle tracker for cascade-mode submenus only.
497
697
  // Drill-in mode replaces the parent view, so there's no "moving toward"
498
698
  // path between two visible menus to protect.
499
- if (!this._drillInMode) {
699
+ if (!wasDrillIn) {
500
700
  // Wait one frame so the submenu's getBoundingClientRect reflects its
501
701
  // final position (computePosition runs in the toggle handler).
502
702
  requestAnimationFrame(() => {
@@ -520,34 +720,210 @@ export class NLDDMenu extends LitElement {
520
720
  this._handleBackMouseenter = () => {
521
721
  this._clearHighlight();
522
722
  };
523
- /** Last cached drill-in mode value, so we only act when it actually
524
- * changes across a resize event. */
525
- this._lastDrillInMode = null;
526
- /** Close any open submenu when the cascade ↔ drill-in threshold is
527
- * crossed during a resize. Switching the rendering of an already-open
528
- * submenu mid-flight (anchor + placement + back button) is more
529
- * disorienting than a clean reset to the root view. */
530
- this._handleViewportResize = () => {
531
- const current = this._drillInMode;
532
- if (this._lastDrillInMode === null) {
533
- this._lastDrillInMode = current;
723
+ this._touchStartX = 0;
724
+ this._touchStartY = 0;
725
+ /** @internal */
726
+ this._handleMenuTouchStart = (event) => {
727
+ const t = event.touches[0];
728
+ if (!t)
534
729
  return;
535
- }
536
- if (this._lastDrillInMode === current)
730
+ this._touchStartX = t.clientX;
731
+ this._touchStartY = t.clientY;
732
+ this.removeAttribute('scroll-active');
733
+ };
734
+ /** @internal */
735
+ this._handleMenuTouchMove = (event) => {
736
+ if (this.hasAttribute('scroll-active'))
537
737
  return;
538
- this._lastDrillInMode = current;
539
- if (this._activeSubmenu) {
540
- this._activeSubmenu.hidePopover?.();
738
+ const t = event.touches[0];
739
+ if (!t)
740
+ return;
741
+ const dx = t.clientX - this._touchStartX;
742
+ const dy = t.clientY - this._touchStartY;
743
+ if (Math.hypot(dx, dy) > NLDDMenu._TOUCH_SCROLL_THRESHOLD_PX) {
744
+ this.setAttribute('scroll-active', '');
541
745
  }
542
746
  };
747
+ /** @internal */
748
+ this._handleMenuTouchEnd = () => {
749
+ this.removeAttribute('scroll-active');
750
+ };
751
+ /** Close the menu on a window resize. Floating-UI's autoUpdate keeps
752
+ * the position in sync with scrolls (where users genuinely expect the
753
+ * menu to follow the anchor), but a full window resize is a layout-
754
+ * shifting interaction the user isn't currently tracking — better to
755
+ * dismiss with a clean slate than to chase the anchor across a
756
+ * reflow, repaint a flipped placement, or get stuck mid-flight when
757
+ * crossing the cascade ↔ drill-in viewport threshold. The window
758
+ * listener is wired only while we're open and torn down on close, so
759
+ * idle menus pay nothing. */
760
+ this._handleWindowResize = () => {
761
+ this._collapseChain();
762
+ };
543
763
  /**
544
- * Close this menu when a `select` event bubbles up selecting an item
545
- * anywhere in the menu (or any descendant submenu) closes the entire
546
- * popover chain so the action feels final. The select event is dispatched
547
- * with `composed: true` so it crosses every ancestor menu in the chain.
764
+ * Collapse the entire menu chain at once root + every open submenu
765
+ * with no "back one level" re-show. Used when the action is final:
766
+ * selecting an item, clicking the anchor, clicking/tapping outside, or
767
+ * a viewport resize.
768
+ *
769
+ * Driven by the root's `_openChain` registry and each submenu's
770
+ * `_collapseSelf`, so it never depends on `_activeSubmenu` /
771
+ * `_parentMenu` threading being intact at collapse time — real
772
+ * interaction can leave those stale, which was the source of the
773
+ * recurring "anchor click doesn't close after the 2nd navigation"
774
+ * bug. Idempotent: every chain level's `select` listener may call it,
775
+ * but the first run empties the registry so the rest are no-ops.
548
776
  */
549
- this._handleSelectChainClose = () => {
550
- this.hidePopover?.();
777
+ this._collapseChain = () => {
778
+ const root = this._chainRoot ?? this;
779
+ // Snapshot + clear first so the re-entrant cleanup splices are
780
+ // no-ops and a trailing same-gesture call finds nothing to do.
781
+ const chain = root._openChain;
782
+ root._openChain = [];
783
+ for (let i = chain.length - 1; i >= 0; i--)
784
+ chain[i]._collapseSelf?.();
785
+ if (root._isOpen)
786
+ root.hidePopover?.();
787
+ };
788
+ /**
789
+ * Drill-in click-outside: collapse the entire chain.
790
+ *
791
+ * In drill-in mode only the deepest level is `:popover-open` — popover
792
+ * light-dismiss would close just that level, and our onToggle cleanup
793
+ * would helpfully re-show the previous level. That's the wrong outcome
794
+ * for a click outside the menu: the user is dismissing the whole menu,
795
+ * not navigating one step back. Detect the outside click here and route
796
+ * to `_collapseChain`, which collapses every level at once.
797
+ *
798
+ * Cascade mode falls back to the native popover stack behaviour: a click
799
+ * outside light-dismisses every popover down to the click target's
800
+ * closest ancestor popover, so no extra handling is needed there.
801
+ *
802
+ * For mouse/pen, pointerdown (capture) is the right hook: it fires
803
+ * before light-dismiss does its work, so by the time the toggle event
804
+ * fires our chain walk has already detached listeners and prevented
805
+ * the parent re-show.
806
+ *
807
+ * For touch, an outside pointerdown is ambiguous: it's also how a page
808
+ * scroll begins. Closing the chain on the scroll-start would be
809
+ * inconsistent with a root menu (which native light-dismiss leaves
810
+ * open while you scroll outside it) and annoying. So on touch we defer
811
+ * the decision: track the pointer and only chain-close on a confirmed
812
+ * tap (pointerup without crossing the scroll threshold); a scroll
813
+ * leaves the chain untouched, matching root-menu behaviour.
814
+ */
815
+ this._dragStartItem = null;
816
+ /**
817
+ * Native HTML `<select>` lets you press on one option, drag to another,
818
+ * and release to pick the one under the pointer. Standard click events
819
+ * don't fire across different elements, so we replicate it manually:
820
+ * remember the pointerdown'd item, listen for the matching pointerup,
821
+ * and if the release lands on a different non-disabled item, fire its
822
+ * `select` directly. The original button's click handler still owns the
823
+ * single-click no-drag path.
824
+ */
825
+ this._handleItemPointerdown = (event) => {
826
+ if (event.button !== 0)
827
+ return;
828
+ const item = event.target?.closest('nldd-menu-item');
829
+ if (!item || item.disabled)
830
+ return;
831
+ this._dragStartItem = item;
832
+ document.addEventListener('pointerup', this._handleDragRelease, { capture: true, once: true });
833
+ };
834
+ this._handleDragRelease = (event) => {
835
+ const startItem = this._dragStartItem;
836
+ this._dragStartItem = null;
837
+ if (!startItem)
838
+ return;
839
+ // If the menu closed between pointerdown and pointerup (light-dismiss,
840
+ // keyboard shortcut), bail — elementFromPoint could otherwise resolve
841
+ // to an item in a different, newly-opened menu and fire its select.
842
+ if (!this._isOpen)
843
+ return;
844
+ const target = document.elementFromPoint(event.clientX, event.clientY);
845
+ const releaseItem = target?.closest('nldd-menu-item');
846
+ if (!releaseItem || releaseItem === startItem || releaseItem.disabled)
847
+ return;
848
+ // Different item — fire its select. Browser won't dispatch click here
849
+ // because pointerdown and pointerup landed on different targets, so
850
+ // there's no double-fire to suppress.
851
+ releaseItem._handleClick();
852
+ };
853
+ this._handleDocumentPointerdown = (event) => {
854
+ if (!this._isOpen)
855
+ return;
856
+ // Anchor pointerdown on a root menu while open: capture as a gesture
857
+ // so the trailing click — fired after native popover light-dismiss
858
+ // has already closed us — won't reopen us via `_handleDocumentClick`.
859
+ // Drill-in submenus take a different path further down.
860
+ if (!this._isSubmenu) {
861
+ const anchorEl = this._getAnchorEl();
862
+ if (anchorEl && event.composedPath().includes(anchorEl)) {
863
+ this._collapsedByPointerGesture = true;
864
+ return;
865
+ }
866
+ }
867
+ if (!this._drillInMode)
868
+ return;
869
+ // Only nested drill-in submenus need this: they form a hidden chain
870
+ // of ancestors that won't auto-collapse on outside click. A root
871
+ // menu has nothing to chain-close — popover light-dismiss + the
872
+ // invoker's `popovertargetaction` handle outside / anchor clicks
873
+ // natively. Without this guard a click on the anchor of a root
874
+ // drill-in menu would fire chain-close → hide → auto-sync flips
875
+ // `popovertargetaction` to 'show' → click default action reopens
876
+ // (the "menu sluit en opent consistent weer" bug on small screens).
877
+ if (!this._isSubmenu)
878
+ return;
879
+ // Walk this menu + every parent in the drill-in chain. If the
880
+ // pointerdown landed inside any of them, it's an in-chain click
881
+ // (e.g. on the back button, on the menu background, on an item)
882
+ // and the existing handlers take care of it.
883
+ const path = event.composedPath();
884
+ let menu = this;
885
+ while (menu) {
886
+ if (path.includes(menu))
887
+ return;
888
+ menu = menu._parentMenu;
889
+ }
890
+ // Touch: defer to a confirmed tap so a page-scroll started outside
891
+ // the chain doesn't collapse it (consistent with the root menu,
892
+ // which stays open while scrolling outside).
893
+ if (event.pointerType === 'touch') {
894
+ this._outsideTapStartX = event.clientX;
895
+ this._outsideTapStartY = event.clientY;
896
+ this._teardownOutsideTap();
897
+ this._outsideTapTracking = true;
898
+ document.addEventListener('pointermove', this._outsideTapMove, true);
899
+ document.addEventListener('pointerup', this._outsideTapEnd, true);
900
+ document.addEventListener('pointercancel', this._outsideTapCancel, true);
901
+ return;
902
+ }
903
+ (this._chainRoot ?? this)._collapsedByPointerGesture = true;
904
+ this._collapseChain();
905
+ };
906
+ this._outsideTapStartX = 0;
907
+ this._outsideTapStartY = 0;
908
+ this._outsideTapTracking = false;
909
+ this._outsideTapMove = (e) => {
910
+ const dx = e.clientX - this._outsideTapStartX;
911
+ const dy = e.clientY - this._outsideTapStartY;
912
+ if (Math.hypot(dx, dy) > NLDDMenu._TOUCH_SCROLL_THRESHOLD_PX) {
913
+ // Became a scroll — leave the chain open.
914
+ this._teardownOutsideTap();
915
+ }
916
+ };
917
+ this._outsideTapEnd = () => {
918
+ const wasTap = this._outsideTapTracking;
919
+ this._teardownOutsideTap();
920
+ if (wasTap) {
921
+ (this._chainRoot ?? this)._collapsedByPointerGesture = true;
922
+ this._collapseChain();
923
+ }
924
+ };
925
+ this._outsideTapCancel = () => {
926
+ this._teardownOutsideTap();
551
927
  };
552
928
  // — Private handlers ——————————————————————————————————————————————————————
553
929
  this._handleKeydown = (event) => {
@@ -645,10 +1021,21 @@ export class NLDDMenu extends LitElement {
645
1021
  this._handleToggle = async (event) => {
646
1022
  const toggleEvent = event;
647
1023
  this._isOpen = toggleEvent.newState === 'open';
1024
+ // Sync the anchor's "is-expanded" state so an anchor button (or any
1025
+ // element exposing an `expanded` property) shows the active visual
1026
+ // while we're open. Skip menu-items as anchors — submenu openers
1027
+ // manage their own `_submenuOpen` lifecycle and don't have an
1028
+ // `expanded` prop. Also opt the anchor into menu/listbox semantics
1029
+ // via `popupType` when it supports it and the consumer hasn't
1030
+ // already chosen a value, so screen readers announce the role.
1031
+ this._syncAnchorPopupState(this._isOpen);
648
1032
  if (toggleEvent.newState !== 'open') {
649
- this._closedAt = Date.now();
650
1033
  this._cleanupAutoUpdate?.();
651
1034
  this._cleanupAutoUpdate = null;
1035
+ window.removeEventListener('resize', this._handleWindowResize);
1036
+ document.removeEventListener('pointerdown', this._handleDocumentPointerdown, true);
1037
+ this._teardownOutsideTap();
1038
+ this._clearHighlight();
652
1039
  return;
653
1040
  }
654
1041
  this._updateDividerVisibility();
@@ -662,6 +1049,14 @@ export class NLDDMenu extends LitElement {
662
1049
  if (anchorEl) {
663
1050
  this._cleanupAutoUpdate = autoUpdate(anchorEl, this, () => this.reposition());
664
1051
  }
1052
+ // Wired only while open so idle menus pay nothing and we don't
1053
+ // accidentally close other menus on the page during a resize or
1054
+ // during an unrelated pointerdown elsewhere.
1055
+ window.addEventListener('resize', this._handleWindowResize, { passive: true });
1056
+ // Capture so we run before the popover light-dismiss algorithm
1057
+ // processes the pointerdown — gives us a chance to redirect the
1058
+ // dismissal into a chain-close in drill-in mode.
1059
+ document.addEventListener('pointerdown', this._handleDocumentPointerdown, true);
665
1060
  await this.updateComplete;
666
1061
  if (this.variant !== 'listbox') {
667
1062
  const keyboard = isKeyboardMode();
@@ -679,8 +1074,14 @@ export class NLDDMenu extends LitElement {
679
1074
  };
680
1075
  }
681
1076
  // — i18n ——————————————————————————————————————————————————————————————————
682
- _t(key) {
683
- return this.translations[key] ?? nlddMenuTranslations[key];
1077
+ _t(key, vars) {
1078
+ let str = this.translations[key] ?? nlddMenuTranslations[key];
1079
+ if (vars) {
1080
+ for (const [k, v] of Object.entries(vars)) {
1081
+ str = str.replace(`{${k}}`, String(v));
1082
+ }
1083
+ }
1084
+ return str;
684
1085
  }
685
1086
  /** Resolved empty text: emptyText attribute takes precedence, then i18n fallback. */
686
1087
  get _resolvedEmptyText() {
@@ -695,22 +1096,54 @@ export class NLDDMenu extends LitElement {
695
1096
  const parent = this._parentItem?.text ?? '';
696
1097
  return parent ? `${back}: ${parent}` : back;
697
1098
  }
1099
+ /**
1100
+ * Write `message` to this menu's polite live region. Clears first, then
1101
+ * sets on a double rAF — the empty→text transition is what makes screen
1102
+ * readers reliably re-announce unchanged-looking content across engines.
1103
+ *
1104
+ * Spam control without muting real navigation: a doubled event in the
1105
+ * same tick (identical message still pending) is dropped; a *different*
1106
+ * message arriving first cancels the stale pending one (rapid navigation
1107
+ * announces only the level you land on); once flushed, the same text may
1108
+ * be announced again for a later genuine transition (e.g. re-entering a
1109
+ * submenu after going back).
1110
+ */
1111
+ _announce(message) {
1112
+ if (!message)
1113
+ return;
1114
+ if (this._announceRaf && message === this._pendingAnnouncement)
1115
+ return;
1116
+ const region = this.shadowRoot?.querySelector('.menu__live-region');
1117
+ if (!region)
1118
+ return;
1119
+ if (this._announceRaf)
1120
+ cancelAnimationFrame(this._announceRaf);
1121
+ this._pendingAnnouncement = message;
1122
+ region.textContent = '';
1123
+ this._announceRaf = requestAnimationFrame(() => {
1124
+ this._announceRaf = requestAnimationFrame(() => {
1125
+ this._announceRaf = 0;
1126
+ this._pendingAnnouncement = '';
1127
+ region.textContent = message;
1128
+ });
1129
+ });
1130
+ }
698
1131
  // — Lifecycle ——————————————————————————————————————————————————————————————
699
1132
  updated(changedProperties) {
700
1133
  if (changedProperties.has('width')) {
701
1134
  if (this.width) {
702
- this.style.setProperty('--_menu-width', this.width);
1135
+ this.style.setProperty('--_width', this.width);
703
1136
  }
704
1137
  else {
705
- this.style.removeProperty('--_menu-width');
1138
+ this.style.removeProperty('--_width');
706
1139
  }
707
1140
  }
708
1141
  if (changedProperties.has('maxItems')) {
709
1142
  if (this.maxItems > 0) {
710
- this.style.setProperty('--_menu-max-items', String(this.maxItems));
1143
+ this.style.setProperty('--_max-items', String(this.maxItems));
711
1144
  }
712
1145
  else {
713
- this.style.removeProperty('--_menu-max-items');
1146
+ this.style.removeProperty('--_max-items');
714
1147
  }
715
1148
  }
716
1149
  if (changedProperties.has('variant')) {
@@ -727,6 +1160,66 @@ export class NLDDMenu extends LitElement {
727
1160
  return document.getElementById(this.anchor);
728
1161
  return null;
729
1162
  }
1163
+ /**
1164
+ * Pick the drill-in side (above vs below the anchor) purely from the
1165
+ * available viewport space around the anchor — never from content
1166
+ * size. Content-independent so every level of the chain resolves to
1167
+ * the same side and a bigger submenu scrolls internally rather than
1168
+ * flipping the whole stack. Keeps the configured alignment suffix
1169
+ * (`-start` / `-end`) from `placement`.
1170
+ */
1171
+ _resolveDrillInPlacement(anchorEl) {
1172
+ const base = this.placement || 'bottom-start';
1173
+ const align = base.includes('-') ? base.slice(base.indexOf('-')) : '';
1174
+ const rect = anchorEl.getBoundingClientRect();
1175
+ const margin = this._cssPx('--_viewport-margin');
1176
+ const spaceBelow = window.innerHeight - rect.bottom - margin;
1177
+ const spaceAbove = rect.top - margin;
1178
+ const side = spaceBelow >= spaceAbove ? 'bottom' : 'top';
1179
+ return `${side}${align}`;
1180
+ }
1181
+ /**
1182
+ * Reflect this menu's open state on the anchor so an anchor button (or
1183
+ * any element exposing an `expanded` IDL property) shows the active
1184
+ * "is-expanded" visual while we're open. Skip menu-items as anchors —
1185
+ * they manage their own `_submenuOpen` lifecycle and don't have an
1186
+ * `expanded` property. Also default `popupType` to 'menu' / 'listbox'
1187
+ * (matching this menu's variant) when the anchor supports it and the
1188
+ * consumer hasn't already chosen a value, so screen readers announce
1189
+ * the popup role without each consumer having to set it manually.
1190
+ *
1191
+ * Finally, drive the anchor's `popoverTargetAction` so the browser's
1192
+ * native invoker action always matches the current state explicitly:
1193
+ * `'hide'` while open, `'show'` while closed. We avoid `'toggle'` on
1194
+ * purpose — toggle re-evaluates the popover's live state at click time
1195
+ * and would re-open the menu if it had just been light-dismissed
1196
+ * milliseconds earlier (the source of the "menu briefly closes and
1197
+ * reopens" bug). With explicit show/hide, the worst case is a no-op
1198
+ * (e.g. `'hide'` on an already-closed menu) — never a spurious open.
1199
+ *
1200
+ * The invoker association (`popoverTargetElement`) is set elsewhere
1201
+ * — persistently on anchor change rather than per-toggle — so the
1202
+ * browser's light-dismiss exclusion is in place from the very first
1203
+ * click instead of only after the menu has already opened once.
1204
+ */
1205
+ _syncAnchorPopupState(isOpen) {
1206
+ const anchor = this._getAnchorEl();
1207
+ if (!anchor)
1208
+ return;
1209
+ if ('expanded' in anchor)
1210
+ anchor.expanded = isOpen;
1211
+ // Seed a valid aria-haspopup when the anchor has no usable popup
1212
+ // type. Falsy covers unset (undefined/null) AND `popup-type=""` —
1213
+ // an empty string is not a valid aria-haspopup token, so it must
1214
+ // still be seeded rather than left invalid. An intentional opt-out
1215
+ // is `popup-type="false"`, which is truthy and preserved here.
1216
+ if (isOpen && 'popupType' in anchor && !anchor.popupType) {
1217
+ anchor.popupType = this.variant === 'listbox' ? 'listbox' : 'menu';
1218
+ }
1219
+ if ('popoverTargetAction' in anchor) {
1220
+ anchor.popoverTargetAction = isOpen ? 'hide' : 'show';
1221
+ }
1222
+ }
730
1223
  /**
731
1224
  * Item-activation logic shared between the native `mouseenter` capture
732
1225
  * handler and the safe-triangle recovery paths (sideways exit, direction
@@ -753,15 +1246,11 @@ export class NLDDMenu extends LitElement {
753
1246
  if (this._movingTowardSubmenu && item !== this._activeSubmenuOpener)
754
1247
  return;
755
1248
  this._setHighlight(item);
756
- // Cursor is on a real item now (not in transit) — cancel any pending
757
- // linger-close from a previous sweep through.
758
- this._cancelHoverClose();
759
1249
  if (this._drillInMode)
760
1250
  return; // Touch / narrow viewport: no hover-open.
761
1251
  // Settled on a peer item (not the active submenu's opener) — close the
762
1252
  // active submenu immediately. If this peer itself has a submenu, the
763
- // hover-open below will then schedule its own. We don't wait for the
764
- // linger-close: the user has already committed to a different item.
1253
+ // hover-open below will then schedule its own.
765
1254
  if (this._activeSubmenu && item !== this._activeSubmenuOpener) {
766
1255
  this._activeSubmenu.hidePopover?.();
767
1256
  }
@@ -824,9 +1313,9 @@ export class NLDDMenu extends LitElement {
824
1313
  this._safeTrianglePinApex(p, openerRect);
825
1314
  }
826
1315
  else {
827
- // 3c. Exited sideways — no protection, fall through to
828
- // linger-close so the submenu closes if cursor leaves
829
- // the menu entirely.
1316
+ // 3c. Exited sideways — no protection. Drop the apex and
1317
+ // activate whatever item is under the cursor so peers
1318
+ // take over immediately.
830
1319
  this._safeTriangleSidewaysExit(p, wasMoving);
831
1320
  }
832
1321
  }
@@ -835,15 +1324,10 @@ export class NLDDMenu extends LitElement {
835
1324
  this._safeTrianglePolygonTest(p, r);
836
1325
  }
837
1326
  this._lastCursorPos = p;
838
- // 5. Common per-frame work: highlight, recovery, timers.
1327
+ // 5. Common per-frame work: highlight, recovery, stall timer.
839
1328
  this._safeTriangleSyncOpenerHighlight();
840
1329
  this._safeTriangleDirectionReversalRecovery(p, wasMoving);
841
1330
  this._safeTriangleScheduleStall();
842
- if (this._movingTowardSubmenu) {
843
- this._cancelHoverClose();
844
- return;
845
- }
846
- this._safeTriangleMaybeScheduleLingerClose(p);
847
1331
  };
848
1332
  window.addEventListener('mousemove', this._safeTriangleListener);
849
1333
  }
@@ -854,7 +1338,6 @@ export class NLDDMenu extends LitElement {
854
1338
  * return trip to the opener and re-pin a fresh apex for another exit. */
855
1339
  _safeTriangleArrived(p) {
856
1340
  this._movingTowardSubmenu = false;
857
- this._cancelHoverClose();
858
1341
  this._lastCursorPos = p;
859
1342
  this._safeTriangleApex = null;
860
1343
  this._activeSubmenuOpener?.removeAttribute('highlighted');
@@ -963,24 +1446,6 @@ export class NLDDMenu extends LitElement {
963
1446
  this._activateItemAt(this._lastCursorPos);
964
1447
  }, NLDDMenu._SAFE_TRIANGLE_STALL_DISMISS_MS);
965
1448
  }
966
- /** Schedule submenu close when cursor sits outside the parent menu rect.
967
- * No-op if cursor is inside the menu (peer-hover logic handles it) or
968
- * if a close is already scheduled. */
969
- _safeTriangleMaybeScheduleLingerClose(p) {
970
- const submenu = this._safeTriangleSubmenu;
971
- if (!submenu)
972
- return;
973
- const rootRect = this.getBoundingClientRect();
974
- const inRoot = NLDDMenu._rectContainsPoint(rootRect, p);
975
- if (!inRoot && this._hoverCloseTimer === null) {
976
- this._hoverCloseTimer = window.setTimeout(() => {
977
- this._hoverCloseTimer = null;
978
- if (this._activeSubmenu === submenu) {
979
- submenu.hidePopover?.();
980
- }
981
- }, NLDDMenu._SUBMENU_LINGER_CLOSE_MS);
982
- }
983
- }
984
1449
  /** True when the rect contains the point (inclusive). Returns false for
985
1450
  * a missing rect or point — convenient for the optional-chained call
986
1451
  * sites in the safe-triangle logic. */
@@ -1038,17 +1503,11 @@ export class NLDDMenu extends LitElement {
1038
1503
  this._hoverOpenTimer = null;
1039
1504
  }
1040
1505
  }
1041
- _cancelHoverClose() {
1042
- if (this._hoverCloseTimer !== null) {
1043
- clearTimeout(this._hoverCloseTimer);
1044
- this._hoverCloseTimer = null;
1045
- }
1046
- }
1047
1506
  /** Recursively checks whether a point sits inside the given menu OR any of
1048
- * its descendant submenus. Used by the hover guard so a cursor deep inside
1049
- * a nested submenu still reads as "in the safe area" for ancestor menus —
1050
- * without this, an ancestor's linger-close fires and cascades the whole
1051
- * chain shut. */
1507
+ * its descendant submenus. Used by the safe-triangle "arrived" check so a
1508
+ * cursor that has continued past the immediate submenu into a deeper
1509
+ * nested submenu still reads as "in the safe area" — without the recursive
1510
+ * walk we'd treat the deeper hover as an exit and re-pin a stale apex. */
1052
1511
  static _isPointInMenuTree(p, menu) {
1053
1512
  const r = menu.getBoundingClientRect();
1054
1513
  if (p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom)
@@ -1100,6 +1559,12 @@ export class NLDDMenu extends LitElement {
1100
1559
  get _isSubmenu() {
1101
1560
  return this._parentMenu !== null;
1102
1561
  }
1562
+ _teardownOutsideTap() {
1563
+ this._outsideTapTracking = false;
1564
+ document.removeEventListener('pointermove', this._outsideTapMove, true);
1565
+ document.removeEventListener('pointerup', this._outsideTapEnd, true);
1566
+ document.removeEventListener('pointercancel', this._outsideTapCancel, true);
1567
+ }
1103
1568
  connectedCallback() {
1104
1569
  super.connectedCallback();
1105
1570
  if (!this.id) {
@@ -1115,15 +1580,9 @@ export class NLDDMenu extends LitElement {
1115
1580
  this.addEventListener('mouseleave', this._handleMouseleave);
1116
1581
  this.addEventListener('menu-item-focused', this._handleMenuItemFocused);
1117
1582
  this.addEventListener('submenu-open', this._handleSubmenuOpen);
1118
- this.addEventListener('select', this._handleSelectChainClose);
1583
+ this.addEventListener('select', this._collapseChain);
1584
+ this.addEventListener('pointerdown', this._handleItemPointerdown);
1119
1585
  document.addEventListener('click', this._handleDocumentClick);
1120
- // Close any open submenu when the viewport crosses the cascade ↔ drill-in
1121
- // threshold mid-session — re-rendering between modes mid-flight would
1122
- // require recomputing anchors and is more disorienting than a clean reset.
1123
- // Subscribe to the MediaQueryList's `change` event rather than every
1124
- // `window.resize` frame: it fires only when the threshold is actually
1125
- // crossed and matches the comment above the cached query.
1126
- NLDDMenu._getDrillInModeQuery().addEventListener('change', this._handleViewportResize);
1127
1586
  }
1128
1587
  firstUpdated() {
1129
1588
  this._updateEmptyState();
@@ -1144,11 +1603,17 @@ export class NLDDMenu extends LitElement {
1144
1603
  this.removeEventListener('mouseleave', this._handleMouseleave);
1145
1604
  this.removeEventListener('menu-item-focused', this._handleMenuItemFocused);
1146
1605
  this.removeEventListener('submenu-open', this._handleSubmenuOpen);
1147
- this.removeEventListener('select', this._handleSelectChainClose);
1606
+ this.removeEventListener('select', this._collapseChain);
1607
+ this.removeEventListener('pointerdown', this._handleItemPointerdown);
1148
1608
  document.removeEventListener('click', this._handleDocumentClick);
1149
- NLDDMenu._getDrillInModeQuery().removeEventListener('change', this._handleViewportResize);
1609
+ document.removeEventListener('pointerup', this._handleDragRelease, true);
1610
+ // Defensive: the pointerdown + resize listeners are added on open
1611
+ // and removed on close, but if the menu is torn down mid-open we'd
1612
+ // otherwise leak the global listeners.
1613
+ document.removeEventListener('pointerdown', this._handleDocumentPointerdown, true);
1614
+ this._teardownOutsideTap();
1615
+ window.removeEventListener('resize', this._handleWindowResize);
1150
1616
  this._cancelHoverOpen();
1151
- this._cancelHoverClose();
1152
1617
  this._stopSafeTriangle();
1153
1618
  if (this._typeaheadTimer !== null) {
1154
1619
  clearTimeout(this._typeaheadTimer);
@@ -1188,24 +1653,44 @@ export class NLDDMenu extends LitElement {
1188
1653
  _updateDividerVisibility() {
1189
1654
  const children = Array.from(this.children);
1190
1655
  children.forEach(el => {
1191
- if (el.tagName.toLowerCase() === 'nldd-menu-divider') {
1656
+ const tag = el.tagName.toLowerCase();
1657
+ if (tag === 'nldd-menu-divider')
1192
1658
  el.removeAttribute('hidden');
1193
- }
1659
+ if (tag === 'nldd-menu-group')
1660
+ el.removeAttribute('data-no-bottom-divider');
1194
1661
  });
1195
1662
  const visible = children.filter(el => !el.hasAttribute('hidden'));
1663
+ // Pass 1: hide redundant explicit dividers (first/last, adjacent to
1664
+ // another divider, adjacent to a group's auto-divider).
1196
1665
  visible.forEach((el, index) => {
1197
1666
  if (el.tagName.toLowerCase() !== 'nldd-menu-divider')
1198
1667
  return;
1668
+ const prevTag = visible[index - 1]?.tagName.toLowerCase();
1669
+ const nextTag = visible[index + 1]?.tagName.toLowerCase();
1199
1670
  const isFirst = index === 0;
1200
1671
  const isLast = index === visible.length - 1;
1201
- const prevIsDivider = index > 0 && visible[index - 1].tagName.toLowerCase() === 'nldd-menu-divider';
1202
- // nldd-menu-group renders its own auto-divider above; suppress an
1203
- // explicit divider that would render right next to it.
1204
- const nextIsGroup = index < visible.length - 1 && visible[index + 1].tagName.toLowerCase() === 'nldd-menu-group';
1205
- if (isFirst || isLast || prevIsDivider || nextIsGroup) {
1672
+ if (isFirst || isLast
1673
+ || prevTag === 'nldd-menu-divider'
1674
+ || prevTag === 'nldd-menu-group'
1675
+ || nextTag === 'nldd-menu-group') {
1206
1676
  el.setAttribute('hidden', '');
1207
1677
  }
1208
1678
  });
1679
+ // Pass 2: determine group bottom-divider against the post-pass-1
1680
+ // visible set. Without this re-filter a group followed by an
1681
+ // explicit divider that pass 1 hid (e.g. because the divider is
1682
+ // last, or the items after it are hidden) would still see the
1683
+ // divider as its "next" sibling and keep the bottom border.
1684
+ const remaining = visible.filter(el => !el.hasAttribute('hidden'));
1685
+ remaining.forEach((el, index) => {
1686
+ if (el.tagName.toLowerCase() !== 'nldd-menu-group')
1687
+ return;
1688
+ const nextTag = remaining[index + 1]?.tagName.toLowerCase();
1689
+ const isLast = index === remaining.length - 1;
1690
+ if (isLast || nextTag === 'nldd-menu-group') {
1691
+ el.setAttribute('data-no-bottom-divider', '');
1692
+ }
1693
+ });
1209
1694
  }
1210
1695
  // — Public API ————————————————————————————————————————————————————————————
1211
1696
  /**
@@ -1294,29 +1779,48 @@ export class NLDDMenu extends LitElement {
1294
1779
  getHighlightedId() {
1295
1780
  return this.getHighlighted()?.id ?? '';
1296
1781
  }
1782
+ /** Read a px-valued CSS custom property as a number. `parseFloat`
1783
+ * keeps fractional pixels; an unset/NaN value falls back to 0. */
1784
+ _cssPx(name) {
1785
+ const v = parseFloat(getComputedStyle(this).getPropertyValue(name));
1786
+ return Number.isNaN(v) ? 0 : v;
1787
+ }
1297
1788
  /** Recalculate position and size relative to the anchor element. */
1298
1789
  async reposition() {
1299
1790
  const anchorEl = this._getAnchorEl();
1300
1791
  if (!anchorEl || !this._isOpen)
1301
1792
  return;
1302
- const viewportMargin = parseInt(getComputedStyle(this).getPropertyValue('--_viewport-margin'));
1793
+ const viewportMargin = this._cssPx('--_viewport-margin');
1303
1794
  // Cascade-mode submenus: shift up by the menu's own padding so the
1304
1795
  // first submenu item lines up vertically with the parent opener item.
1305
1796
  // Without this, the submenu's top edge aligns with the opener and the
1306
1797
  // inner padding pushes the first item down, leaving a visible step.
1307
1798
  const submenuPadding = (this._isSubmenu && !this._drillInMode)
1308
- ? parseInt(getComputedStyle(this).getPropertyValue('--_menu-padding')) || 0
1799
+ ? this._cssPx('--_padding')
1309
1800
  : 0;
1801
+ // Drill-in: pick the side from the available space around the
1802
+ // anchor — recomputed every reposition, but content-independent so
1803
+ // a larger submenu never changes it (no mid-chain jump). `flip` is
1804
+ // content-dependent and WOULD jump the stack when a bigger submenu
1805
+ // doesn't fit, so it's dropped here; `size` caps the height and
1806
+ // the menu scrolls internally instead. Re-resolving each time (vs
1807
+ // freezing) keeps it following the anchor on scroll and re-picking
1808
+ // the roomiest side after a close + reopen. Cascade keeps `flip`
1809
+ // (submenus open beside the opener, content-fit matters there).
1810
+ const drillIn = this._drillInMode;
1811
+ const effectivePlacement = drillIn
1812
+ ? this._resolveDrillInPlacement(anchorEl)
1813
+ : this.placement;
1310
1814
  const { x, y } = await computePosition(anchorEl, this, {
1311
- placement: this.placement,
1815
+ placement: effectivePlacement,
1312
1816
  middleware: [
1313
1817
  offset({ mainAxis: 0, alignmentAxis: -submenuPadding }),
1314
- flip({ padding: viewportMargin }),
1818
+ ...(drillIn ? [] : [flip({ padding: viewportMargin })]),
1315
1819
  shift({ padding: viewportMargin }),
1316
1820
  size({
1317
1821
  padding: viewportMargin,
1318
1822
  apply: ({ availableHeight }) => {
1319
- this.style.setProperty('--_menu-max-height', `${availableHeight}px`);
1823
+ this.style.setProperty('--_max-height', `${availableHeight}px`);
1320
1824
  },
1321
1825
  }),
1322
1826
  ],
@@ -1367,14 +1871,11 @@ export class NLDDMenu extends LitElement {
1367
1871
  }
1368
1872
  }
1369
1873
  NLDDMenu.styles = menuStyles;
1370
- /** Wait this long after cursor stops being on a path toward the submenu
1371
- * before closing. Smooths over brief cursor stalls and accidental nudges. */
1372
- NLDDMenu._SUBMENU_LINGER_CLOSE_MS = 300;
1373
1874
  /** When the cursor sits motionless inside the safe triangle for this
1374
1875
  * long, dismiss the triangle and let whatever item is under the cursor
1375
1876
  * become interactive. Without this, a paused cursor would stay
1376
1877
  * "protected" indefinitely and the user couldn't peer-hover. */
1377
- NLDDMenu._SAFE_TRIANGLE_STALL_DISMISS_MS = 750;
1878
+ NLDDMenu._SAFE_TRIANGLE_STALL_DISMISS_MS = 500;
1378
1879
  /** Pull the wedge apex this many pixels away from the submenu edge (i.e.
1379
1880
  * to the left when the submenu is on the right). Widens the wedge near
1380
1881
  * the cursor so brief diagonal wobble doesn't pop the cursor out at the
@@ -1395,10 +1896,20 @@ NLDDMenu._SAFE_TRIANGLE_APEX_X_OFFSET = 4;
1395
1896
  *
1396
1897
  * Cached MediaQueryList shared across instances — `matchMedia()` returns
1397
1898
  * a live object and re-evaluating it for every mouseenter/submenu-open
1398
- * is wasteful. Instances also subscribe to `change` so resize events
1399
- * naturally route through `_handleViewportResize`.
1899
+ * is wasteful.
1400
1900
  */
1401
1901
  NLDDMenu._drillInModeQuery = null;
1902
+ // — Touch-scroll press-flash suppression ————————————————————————————————
1903
+ //
1904
+ // On touch, pressing an item sets :active and the synthetic sticky
1905
+ // :hover lands on it, so `.menu__item:active:hover` flashes. If the
1906
+ // gesture becomes an internal scroll (cramped menu), :active lingers
1907
+ // until the browser recognises the pan and the highlight stays for
1908
+ // the whole scroll. We detect a scroll from the touch delta and set
1909
+ // `scroll-active` on the host; the styles neutralise the press flash
1910
+ // while it's set. A pure tap never moves past the threshold, so its
1911
+ // brief press feedback is preserved.
1912
+ NLDDMenu._TOUCH_SCROLL_THRESHOLD_PX = 8;
1402
1913
  // — Lifecycle callbacks ————————————————————————————————————————————————————
1403
1914
  NLDDMenu._menuIdCounter = 0;
1404
1915
  /** Reset the typeahead buffer after this much idle time. 500ms is the