@ds-mo/ui 14.18.0 → 14.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +7 -0
  2. package/dist/.package-ready.json +1 -1
  3. package/dist/agent-patterns.json +4 -3
  4. package/dist/agent.json +44 -14
  5. package/dist/angular/ds-filter-menu.d.ts +1 -1
  6. package/dist/angular/ds-filter-menu.js +3 -3
  7. package/dist/angular/ds-input.d.ts +1 -1
  8. package/dist/angular/ds-input.js +3 -3
  9. package/dist/angular/ds-menu.d.ts +1 -1
  10. package/dist/angular/ds-menu.js +3 -3
  11. package/dist/angular/ds-panel-tool-header.d.ts +3 -8
  12. package/dist/angular/ds-panel-tools.d.ts +3 -8
  13. package/dist/angular/ds-select.d.ts +1 -1
  14. package/dist/angular/ds-select.js +3 -3
  15. package/dist/angular/ds-tooltip.d.ts +1 -1
  16. package/dist/angular/ds-tooltip.js +3 -3
  17. package/dist/components/ds-agent-activity.js +1 -1
  18. package/dist/components/ds-agent-questionnaire.js +1 -1
  19. package/dist/components/ds-agent-response.js +1 -1
  20. package/dist/components/ds-agent-source-list.js +1 -1
  21. package/dist/components/ds-agent-tool-call.js +1 -1
  22. package/dist/components/ds-attachment-list.js +1 -1
  23. package/dist/components/ds-badge.js +1 -1
  24. package/dist/components/ds-banner.js +1 -1
  25. package/dist/components/ds-banner.js.map +1 -1
  26. package/dist/components/ds-bar-action.js +1 -1
  27. package/dist/components/ds-bar-action.js.map +1 -1
  28. package/dist/components/ds-bar-nav.js +1 -1
  29. package/dist/components/ds-bar-title.js +1 -1
  30. package/dist/components/ds-bar-workflow.js +1 -1
  31. package/dist/components/ds-bar-workflow.js.map +1 -1
  32. package/dist/components/ds-breadcrumb.js +1 -1
  33. package/dist/components/ds-button-filled.js +1 -1
  34. package/dist/components/ds-button-unfilled.js +1 -1
  35. package/dist/components/ds-card-chart.js +1 -1
  36. package/dist/components/ds-card-overview.js +1 -1
  37. package/dist/components/ds-card-setting.js +1 -1
  38. package/dist/components/ds-chart-legend.js +1 -1
  39. package/dist/components/ds-chart.js +1 -1
  40. package/dist/components/ds-checkbox.js +1 -1
  41. package/dist/components/ds-chip.js +1 -1
  42. package/dist/components/ds-code-block.js +1 -1
  43. package/dist/components/ds-conversation-list-item.js +1 -1
  44. package/dist/components/ds-conversation-list-section.js +1 -1
  45. package/dist/components/ds-conversation-list.js +1 -1
  46. package/dist/components/ds-conversation-list.js.map +1 -1
  47. package/dist/components/ds-divider.js +1 -1
  48. package/dist/components/ds-empty-state.js +1 -1
  49. package/dist/components/ds-field.js +1 -1
  50. package/dist/components/ds-filter-menu.js +1 -1
  51. package/dist/components/ds-input.js +1 -1
  52. package/dist/components/ds-markdown.js +1 -1
  53. package/dist/components/ds-menu.js +1 -1
  54. package/dist/components/ds-message-actions.js +1 -1
  55. package/dist/components/ds-message-bubble.js +1 -1
  56. package/dist/components/ds-message-composer.js +1 -1
  57. package/dist/components/ds-message-composer.js.map +1 -1
  58. package/dist/components/ds-message-scroller.js +1 -1
  59. package/dist/components/ds-message.js +1 -1
  60. package/dist/components/ds-mobile-bar-nav.js +1 -1
  61. package/dist/components/ds-mobile-header.js +1 -1
  62. package/dist/components/ds-mobile-section-switcher.js +1 -1
  63. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  64. package/dist/components/ds-modal.js +1 -1
  65. package/dist/components/ds-pagination.js +1 -1
  66. package/dist/components/ds-panel-nav.js +1 -1
  67. package/dist/components/ds-panel-sub-nav.js +1 -1
  68. package/dist/components/ds-panel-tool-header.js +1 -1
  69. package/dist/components/ds-panel-tool-search.js +1 -1
  70. package/dist/components/ds-panel-tools.js +1 -1
  71. package/dist/components/ds-paper-texture.js +1 -1
  72. package/dist/components/ds-radio.js +1 -1
  73. package/dist/components/ds-scroll-overlay.js +1 -1
  74. package/dist/components/ds-select.js +1 -1
  75. package/dist/components/ds-shell-app.js +1 -1
  76. package/dist/components/ds-shell-app.js.map +1 -1
  77. package/dist/components/ds-shell-page.js +1 -1
  78. package/dist/components/ds-shell-tools.js +1 -1
  79. package/dist/components/ds-skeleton.js +1 -1
  80. package/dist/components/ds-slider.js +1 -1
  81. package/dist/components/ds-swatch-picker.js +1 -1
  82. package/dist/components/ds-switch.js +1 -1
  83. package/dist/components/ds-tab-group.js +1 -1
  84. package/dist/components/ds-table-filter.js +1 -1
  85. package/dist/components/ds-table-saved-views.js +1 -1
  86. package/dist/components/ds-table-sort.js +1 -1
  87. package/dist/components/ds-table-toolbar.js +1 -1
  88. package/dist/components/ds-table.js +1 -1
  89. package/dist/components/ds-table.js.map +1 -1
  90. package/dist/components/ds-tag.js +1 -1
  91. package/dist/components/ds-text.js +1 -1
  92. package/dist/components/ds-toast.js +1 -1
  93. package/dist/components/ds-toast.js.map +1 -1
  94. package/dist/components/ds-tooltip-chart.js +1 -1
  95. package/dist/components/ds-tooltip.js +1 -1
  96. package/dist/components/ds-typing-indicator.js +1 -1
  97. package/dist/components/ds-typing-indicator.js.map +1 -1
  98. package/dist/components/{p-Dginv8PT.js → p-1Hu9jlju.js} +2 -2
  99. package/dist/components/{p-BiBXCjjo.js → p-B00PEE6t.js} +2 -2
  100. package/dist/components/{p-C6yLERx6.js → p-B1JRCDHS.js} +2 -2
  101. package/dist/components/p-BBG6Sswb.js +2 -0
  102. package/dist/components/{p-Drcl4zS9.js.map → p-BBG6Sswb.js.map} +1 -1
  103. package/dist/components/{p-Dd8VKdAO.js → p-BCOl1x0_.js} +2 -2
  104. package/dist/components/{p-C9StMCWg.js → p-BHVOiWz5.js} +2 -2
  105. package/dist/components/{p-DJ0GCCFa.js → p-BNkU9dcc.js} +2 -2
  106. package/dist/components/p-BPG0r_mC.js +2 -0
  107. package/dist/components/p-BPG0r_mC.js.map +1 -0
  108. package/dist/components/p-BUDAwJVV.js +2 -0
  109. package/dist/components/p-BUDAwJVV.js.map +1 -0
  110. package/dist/components/{p-hp_2W2Qy.js → p-BXN-e7vF.js} +2 -2
  111. package/dist/components/{p-CihNoe1M.js → p-B_jZIC9R.js} +2 -2
  112. package/dist/components/{p-CAJIARNW.js → p-Bbb9A3NV.js} +2 -2
  113. package/dist/components/{p-jk2-y5dH.js → p-Bece5KH3.js} +2 -2
  114. package/dist/components/{p-D6P_1YrL.js → p-BhYS_Ump.js} +2 -2
  115. package/dist/components/p-Bkh7scMQ.js +2 -0
  116. package/dist/components/{p-DMaQkEYP.js.map → p-Bkh7scMQ.js.map} +1 -1
  117. package/dist/components/p-BknXYAEc.js +2 -0
  118. package/dist/components/{p-9GuNIXuz.js.map → p-BknXYAEc.js.map} +1 -1
  119. package/dist/components/{p-BKx2ihnG.js → p-Bu3dOjpW.js} +2 -2
  120. package/dist/components/{p-CBa_6lqj.js → p-C-Y7gYgz.js} +2 -2
  121. package/dist/components/{p-9EQ3xLI1.js → p-C6R5Lft0.js} +2 -2
  122. package/dist/components/{p-DPR0TGGJ.js → p-C9gC4haA.js} +2 -2
  123. package/dist/components/{p-LHzBS95u.js → p-CAnnQL1F.js} +2 -2
  124. package/dist/components/p-CE2IKOsy.js +2 -0
  125. package/dist/components/{p-DaxKVtZG.js.map → p-CE2IKOsy.js.map} +1 -1
  126. package/dist/components/{p-DHzn7INS.js → p-CKVHrOEp.js} +2 -2
  127. package/dist/components/{p-CbZ0RU2c.js → p-CLVxgTme.js} +2 -2
  128. package/dist/components/p-CY4YFAT3.js +2 -0
  129. package/dist/components/p-CY4YFAT3.js.map +1 -0
  130. package/dist/components/{p-3WlVHGUe.js → p-Cg0kimXZ.js} +2 -2
  131. package/dist/components/p-CqQFowZd.js +2 -0
  132. package/dist/components/p-CqQFowZd.js.map +1 -0
  133. package/dist/components/{p-HZz5aJKz.js → p-CsBICYmt.js} +2 -2
  134. package/dist/components/p-CwjVgUqy.js +2 -0
  135. package/dist/components/p-CwjVgUqy.js.map +1 -0
  136. package/dist/components/{p-Dl3gciej.js → p-D123Jvp-.js} +2 -2
  137. package/dist/components/p-D1eEgwcP.js +2 -0
  138. package/dist/components/p-D1eEgwcP.js.map +1 -0
  139. package/dist/components/{p-oCc3mz_c.js → p-D3fMaUJ3.js} +2 -2
  140. package/dist/components/p-D7R4yE2C.js +2 -0
  141. package/dist/components/{p-Ba7DaFkb.js.map → p-D7R4yE2C.js.map} +1 -1
  142. package/dist/components/{p-B9ZfoprK.js → p-D7haeL5y.js} +2 -2
  143. package/dist/components/p-D7haeL5y.js.map +1 -0
  144. package/dist/components/{p-CHEt4mW8.js → p-DAvh38E4.js} +2 -2
  145. package/dist/components/{p-DPpHSIzS.js → p-DBue3ISk.js} +2 -2
  146. package/dist/components/{p-BdgoB2ef.js → p-DDgeXGbB.js} +2 -2
  147. package/dist/components/{p-Be-vZpWx.js → p-DUdOmSFR.js} +2 -2
  148. package/dist/components/p-DtndC9f1.js +2 -0
  149. package/dist/components/p-DtndC9f1.js.map +1 -0
  150. package/dist/components/{p-BTTxl3ro.js → p-H5T258T-.js} +2 -2
  151. package/dist/components/p-H5T258T-.js.map +1 -0
  152. package/dist/components/p-HB_cf_aR.js +2 -0
  153. package/dist/components/{p-BBmRD_S0.js.map → p-HB_cf_aR.js.map} +1 -1
  154. package/dist/components/p-LK-jpxwq.js +2 -0
  155. package/dist/components/p-LK-jpxwq.js.map +1 -0
  156. package/dist/components/p-ag6Io-kL.js +2 -0
  157. package/dist/components/p-ag6Io-kL.js.map +1 -0
  158. package/dist/components/p-iMuFgSoM.js +2 -0
  159. package/dist/components/p-iMuFgSoM.js.map +1 -0
  160. package/dist/components/{p-B145kX-i.js → p-uI6z53BE.js} +2 -2
  161. package/dist/docs/components.json +223 -37
  162. package/dist/lib/components/PanelTools/panel-tools-types.d.ts +12 -0
  163. package/dist/lib/shell/index.d.ts +1 -1
  164. package/dist/lib/shell/index.js.map +1 -1
  165. package/dist/lib/utils/anchored-position.d.ts +6 -2
  166. package/dist/mcp-data/registry/badge.json +7 -9
  167. package/dist/mcp-data/registry/banner.json +2 -2
  168. package/dist/mcp-data/registry/bar-action.json +2 -2
  169. package/dist/mcp-data/registry/button-unfilled.json +1 -1
  170. package/dist/mcp-data/registry/conversation-list.json +2 -2
  171. package/dist/mcp-data/registry/filter-menu.json +21 -6
  172. package/dist/mcp-data/registry/input.json +23 -3
  173. package/dist/mcp-data/registry/menu.json +22 -4
  174. package/dist/mcp-data/registry/message-composer.json +2 -2
  175. package/dist/mcp-data/registry/panel-nav.json +1 -1
  176. package/dist/mcp-data/registry/panel-tool-header.json +3 -3
  177. package/dist/mcp-data/registry/panel-tools.json +5 -4
  178. package/dist/mcp-data/registry/registry.json +43 -13
  179. package/dist/mcp-data/registry/select.json +17 -3
  180. package/dist/mcp-data/registry/shell-app.json +3 -2
  181. package/dist/mcp-data/registry/table.json +21 -1
  182. package/dist/mcp-data/registry/toast.json +2 -2
  183. package/dist/mcp-data/registry/tooltip.json +20 -3
  184. package/dist/mcp-data/registry/typing-indicator.json +4 -4
  185. package/dist/react/ds-panel-tool-header.d.ts +2 -5
  186. package/dist/react/ds-panel-tools.d.ts +2 -5
  187. package/dist/types/components/Badge/Badge.d.ts +2 -16
  188. package/dist/types/components/Banner/Banner.d.ts +0 -1
  189. package/dist/types/components/BarAction/BarAction.d.ts +0 -7
  190. package/dist/types/components/ConversationList/ConversationList.d.ts +1 -0
  191. package/dist/types/components/FilterMenu/FilterMenu.d.ts +5 -5
  192. package/dist/types/components/Input/Input.d.ts +7 -0
  193. package/dist/types/components/Menu/Menu.d.ts +3 -4
  194. package/dist/types/components/MessageComposer/MessageComposer.d.ts +0 -3
  195. package/dist/types/components/PanelToolHeader/PanelToolHeader.d.ts +2 -5
  196. package/dist/types/components/PanelTools/PanelTools.d.ts +2 -5
  197. package/dist/types/components/PanelTools/panel-tools-types.d.ts +12 -0
  198. package/dist/types/components/Select/Select.d.ts +3 -0
  199. package/dist/types/components/Table/Table.d.ts +1 -13
  200. package/dist/types/components/Table/table-body-renderer.d.ts +36 -0
  201. package/dist/types/components/Table/table-load-view.d.ts +17 -0
  202. package/dist/types/components/Table/table-row-view.d.ts +26 -0
  203. package/dist/types/components/Table/table-skeleton-view.d.ts +12 -0
  204. package/dist/types/components/Tooltip/Tooltip.d.ts +3 -0
  205. package/dist/types/components.d.ts +61 -23
  206. package/dist/types/shell/index.d.ts +1 -1
  207. package/dist/types/utils/anchored-overlay-boundary.d.ts +14 -0
  208. package/dist/types/utils/anchored-overlay-interaction-controller.d.ts +26 -0
  209. package/dist/types/utils/anchored-position-controller.d.ts +14 -0
  210. package/dist/types/utils/anchored-position.d.ts +6 -2
  211. package/dist/types/utils/select-controller.d.ts +1 -0
  212. package/dist/vue/ds-filter-menu.js +1 -0
  213. package/dist/vue/ds-input.js +1 -0
  214. package/dist/vue/ds-menu.js +1 -0
  215. package/dist/vue/ds-select.js +1 -0
  216. package/dist/vue/ds-tooltip.js +1 -0
  217. package/package.json +4 -1
  218. package/dist/components/p-9GuNIXuz.js +0 -2
  219. package/dist/components/p-B9ZfoprK.js.map +0 -1
  220. package/dist/components/p-BBmRD_S0.js +0 -2
  221. package/dist/components/p-BTTxl3ro.js.map +0 -1
  222. package/dist/components/p-Ba7DaFkb.js +0 -2
  223. package/dist/components/p-BcNYvLMi.js +0 -2
  224. package/dist/components/p-BcNYvLMi.js.map +0 -1
  225. package/dist/components/p-CI5Ce8VF.js +0 -2
  226. package/dist/components/p-CI5Ce8VF.js.map +0 -1
  227. package/dist/components/p-Cgm7YMLw.js +0 -2
  228. package/dist/components/p-Cgm7YMLw.js.map +0 -1
  229. package/dist/components/p-Cj5Hkt6Y.js +0 -2
  230. package/dist/components/p-Cj5Hkt6Y.js.map +0 -1
  231. package/dist/components/p-DC_8dK55.js +0 -2
  232. package/dist/components/p-DC_8dK55.js.map +0 -1
  233. package/dist/components/p-DKY3IJqt.js +0 -2
  234. package/dist/components/p-DKY3IJqt.js.map +0 -1
  235. package/dist/components/p-DMaQkEYP.js +0 -2
  236. package/dist/components/p-DQyvAN44.js +0 -2
  237. package/dist/components/p-DQyvAN44.js.map +0 -1
  238. package/dist/components/p-DaxKVtZG.js +0 -2
  239. package/dist/components/p-DbJahF86.js +0 -2
  240. package/dist/components/p-DbJahF86.js.map +0 -1
  241. package/dist/components/p-DhKvqh5G.js +0 -2
  242. package/dist/components/p-DhKvqh5G.js.map +0 -1
  243. package/dist/components/p-Drcl4zS9.js +0 -2
  244. package/dist/components/p-DzPw6aDK.js +0 -2
  245. package/dist/components/p-DzPw6aDK.js.map +0 -1
  246. package/dist/types/shell/badge-gradient-ring.d.ts +0 -28
  247. /package/dist/components/{p-Dginv8PT.js.map → p-1Hu9jlju.js.map} +0 -0
  248. /package/dist/components/{p-BiBXCjjo.js.map → p-B00PEE6t.js.map} +0 -0
  249. /package/dist/components/{p-C6yLERx6.js.map → p-B1JRCDHS.js.map} +0 -0
  250. /package/dist/components/{p-Dd8VKdAO.js.map → p-BCOl1x0_.js.map} +0 -0
  251. /package/dist/components/{p-C9StMCWg.js.map → p-BHVOiWz5.js.map} +0 -0
  252. /package/dist/components/{p-DJ0GCCFa.js.map → p-BNkU9dcc.js.map} +0 -0
  253. /package/dist/components/{p-hp_2W2Qy.js.map → p-BXN-e7vF.js.map} +0 -0
  254. /package/dist/components/{p-CihNoe1M.js.map → p-B_jZIC9R.js.map} +0 -0
  255. /package/dist/components/{p-CAJIARNW.js.map → p-Bbb9A3NV.js.map} +0 -0
  256. /package/dist/components/{p-jk2-y5dH.js.map → p-Bece5KH3.js.map} +0 -0
  257. /package/dist/components/{p-D6P_1YrL.js.map → p-BhYS_Ump.js.map} +0 -0
  258. /package/dist/components/{p-BKx2ihnG.js.map → p-Bu3dOjpW.js.map} +0 -0
  259. /package/dist/components/{p-CBa_6lqj.js.map → p-C-Y7gYgz.js.map} +0 -0
  260. /package/dist/components/{p-9EQ3xLI1.js.map → p-C6R5Lft0.js.map} +0 -0
  261. /package/dist/components/{p-DPR0TGGJ.js.map → p-C9gC4haA.js.map} +0 -0
  262. /package/dist/components/{p-LHzBS95u.js.map → p-CAnnQL1F.js.map} +0 -0
  263. /package/dist/components/{p-DHzn7INS.js.map → p-CKVHrOEp.js.map} +0 -0
  264. /package/dist/components/{p-CbZ0RU2c.js.map → p-CLVxgTme.js.map} +0 -0
  265. /package/dist/components/{p-3WlVHGUe.js.map → p-Cg0kimXZ.js.map} +0 -0
  266. /package/dist/components/{p-HZz5aJKz.js.map → p-CsBICYmt.js.map} +0 -0
  267. /package/dist/components/{p-Dl3gciej.js.map → p-D123Jvp-.js.map} +0 -0
  268. /package/dist/components/{p-oCc3mz_c.js.map → p-D3fMaUJ3.js.map} +0 -0
  269. /package/dist/components/{p-CHEt4mW8.js.map → p-DAvh38E4.js.map} +0 -0
  270. /package/dist/components/{p-DPpHSIzS.js.map → p-DBue3ISk.js.map} +0 -0
  271. /package/dist/components/{p-BdgoB2ef.js.map → p-DDgeXGbB.js.map} +0 -0
  272. /package/dist/components/{p-Be-vZpWx.js.map → p-DUdOmSFR.js.map} +0 -0
  273. /package/dist/components/{p-B145kX-i.js.map → p-uI6z53BE.js.map} +0 -0
@@ -44,6 +44,7 @@
44
44
  "The built-in select trigger exposes combobox semantics with a dialog popup, synchronizes expanded state, holds the pressed interaction wash without changing resting foreground while open, and restores focus after dismissal.",
45
45
  "The category pane is a vertical tab list. Arrow Up and Arrow Down wrap, Home and End jump, Arrow Right enters the option pane, and Arrow Left returns to the active category.",
46
46
  "Escape and outside activation close the popup and restore trigger focus. Tab moves through option controls and closes only when it leaves the popup boundary.",
47
+ "When an external trigger is inside an open shadow tree, pass its HTMLElement through anchor so outside activation, focus return, and Tab continuation use the composed tree.",
47
48
  "The trigger count reports active filter categories. Category counts and the footer report individual selected criteria; none replace visible selected-state indication."
48
49
  ],
49
50
  "states": [
@@ -54,10 +55,10 @@
54
55
  "The trigger reports how many filter categories have selections, while the footer appears when values are selected and reports the total selected criteria."
55
56
  ],
56
57
  "responsiveBehavior": [
57
- "The popup defaults to the large menu-width token and clamps to the viewport.",
58
+ "The popup defaults to the large menu-width token and clamps to the nearest data-ds-overlay-boundary rectangle or the viewport fallback.",
58
59
  "When collapseLabel is set inside a named ds-table container narrower than 900px, the trigger omits its label and chevron, becomes icon-only, and exposes its label through a hover and keyboard-focus tooltip. Active criteria promote the prefix icon to primary.",
59
60
  "Both panes remain visible at narrow widths, with a smaller category minimum and reduced option-pane padding.",
60
- "Treat side as preferred placement; the popup flips and clamps when viewport collision requires it."
61
+ "Treat side as preferred placement; the popup flips and clamps when boundary collision requires it. Assign boundary as an HTMLElement only for an explicit override."
61
62
  ],
62
63
  "frameworkCaveats": {
63
64
  "customElements": [
@@ -138,6 +139,13 @@
138
139
  "mutable": false,
139
140
  "description": "Direct accessible name for the internal select trigger."
140
141
  },
142
+ "boundary": {
143
+ "type": "HTMLElement | undefined",
144
+ "resolvedType": "HTMLElement | undefined",
145
+ "required": false,
146
+ "mutable": false,
147
+ "description": "Explicit collision owner; otherwise the nearest data-ds-overlay-boundary ancestor is used."
148
+ },
141
149
  "categoriesLabel": {
142
150
  "type": "string",
143
151
  "resolvedType": "string",
@@ -224,7 +232,7 @@
224
232
  "default": "TOKEN_CSS_LENGTHS.menuWidthLg",
225
233
  "required": false,
226
234
  "mutable": false,
227
- "description": "Popup width. It remains clamped to the viewport by the component recipe."
235
+ "description": "Popup width. It remains clamped to its collision boundary by the component recipe."
228
236
  },
229
237
  "open": {
230
238
  "type": "boolean",
@@ -402,6 +410,13 @@
402
410
  "mutable": false,
403
411
  "description": "Direct accessible name for the internal select trigger."
404
412
  },
413
+ "boundary": {
414
+ "type": "HTMLElement | undefined",
415
+ "resolvedType": "HTMLElement | undefined",
416
+ "required": false,
417
+ "mutable": false,
418
+ "description": "Explicit collision owner; otherwise the nearest data-ds-overlay-boundary ancestor is used."
419
+ },
405
420
  "categoriesLabel": {
406
421
  "type": "string",
407
422
  "resolvedType": "string",
@@ -488,7 +503,7 @@
488
503
  "default": "TOKEN_CSS_LENGTHS.menuWidthLg",
489
504
  "required": false,
490
505
  "mutable": false,
491
- "description": "Popup width. It remains clamped to the viewport by the component recipe."
506
+ "description": "Popup width. It remains clamped to its collision boundary by the component recipe."
492
507
  },
493
508
  "open": {
494
509
  "type": "boolean",
@@ -630,7 +645,7 @@
630
645
  "import": "import { DsFilterMenu } from '@ds-mo/ui/angular/ds-filter-menu';",
631
646
  "example": "<ds-filter-menu></ds-filter-menu>"
632
647
  },
633
- "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, filters, values.",
648
+ "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, boundary, filters, values.",
634
649
  "peerDependencies": {
635
650
  "required": [
636
651
  "@ds-mo/tokens ^6.5.0",
@@ -661,7 +676,7 @@
661
676
  },
662
677
  {
663
678
  "path": "src/wc/components/FilterMenu/FilterMenu.tsx",
664
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n CONTROL_TEXT_VARIANT,\n controlWidthClass,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ControlWidth,\n} from '../../utils';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport type { AnchoredAlign, AnchoredSide } from '../../utils/anchored-position';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { ChoiceOptionRow } from '../../utils/choice-list-parts';\nimport { choiceListUsesSubtext } from '../../utils/choice-list';\n\nexport type FilterMenuFilterKind = 'single' | 'multiple' | 'boolean' | 'date';\nexport type FilterMenuSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type FilterMenuWidth = ControlWidth;\n\nexport interface FilterMenuOption {\n label: string;\n value: string;\n description?: string;\n isInactive?: boolean;\n}\n\nexport interface FilterMenuFilter {\n id: string;\n label: string;\n kind: FilterMenuFilterKind;\n options?: FilterMenuOption[];\n description?: string;\n fieldLabel?: string;\n /** Draw a standard menu-section divider after this category. */\n divider?: boolean;\n}\n\nexport type FilterMenuValue = string[] | boolean | string;\nexport type FilterMenuValues = Record<string, FilterMenuValue | undefined>;\n\nexport interface FilterMenuChangeDetail {\n filterId: string;\n value: FilterMenuValue;\n}\n\nconst FILTER_MENU_FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nlet filterMenuId = 0;\n\nconst ICON_SIZE: Record<FilterMenuSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\n@Component({\n tag: 'ds-filter-menu',\n styleUrl: 'FilterMenu.css',\n scoped: true,\n})\nexport class FilterMenu {\n @Element() el!: HTMLElement;\n\n /** Controlled popup visibility. */\n @Prop({ mutable: true }) open: boolean = false;\n /** Select trigger text. The selected count is appended automatically. */\n @Prop() triggerLabel: string = 'Filters';\n /** Optional select trigger prefix icon name. */\n @Prop() icon: string | undefined = 'Filters';\n /** Select trigger density. */\n @Prop() size: FilterMenuSize = 'md';\n /** Select trigger width fit. */\n @Prop() width: FilterMenuWidth = 'hug';\n /** Show the surface-aware inset border around the select trigger. */\n @Prop() hasBorder: boolean = true;\n /** Show selected interaction fill when one or more criteria are active. */\n @Prop() activeFill: boolean = false;\n /**\n * Opt into table-caption icon-only chrome below 900px. The trigger omits its\n * visible label and chevron; keep an accessible name via aria-label.\n */\n @Prop({ reflect: true }) collapseLabel: boolean = false;\n /** ID applied to the internal select trigger. */\n @Prop() inputId: string | undefined;\n /** Direct accessible name for the internal select trigger. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Product-owned filter categories and option definitions. */\n @Prop() filters: FilterMenuFilter[] = [];\n /** Controlled values keyed by filter id. */\n @Prop() values: FilterMenuValues = {};\n /** Controlled category shown in the option pane. */\n @Prop() activeFilterId: string | undefined;\n /** External trigger element to position against. */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element used for positioning and focus return. */\n @Prop() anchorId: string | undefined;\n /** Preferred popup side; collision handling may flip it. */\n @Prop() side: AnchoredSide = 'bottom';\n /** Cross-axis alignment against the trigger. */\n @Prop() align: AnchoredAlign = 'end';\n /** Gap between trigger and popup — number in px or a TokoMo length. */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number in px or a TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n /** Popup width. It remains clamped to the viewport by the component recipe. */\n @Prop() menuWidth: string = TOKEN_CSS_LENGTHS.menuWidthLg;\n /** Accessible name for the non-modal filter dialog. */\n @Prop() menuLabel: string = 'Filters';\n /** Accessible name for the category tab list. */\n @Prop() categoriesLabel: string = 'Filter categories';\n /** Footer action and date-clear accessible label. */\n @Prop() clearLabel: string = 'Clear';\n /** Show a visible focus ring on initial entry after keyboard activation. */\n @Prop() initialFocusVisible: boolean = false;\n\n @State() private shouldRender = false;\n @State() private closing = false;\n @State() private positionReady = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusRingVisible = false;\n @State() private activeOptionIndex = 0;\n @State() private captionCompact = false;\n\n /** Requests a controlled value replacement without closing the popup. */\n @Event() dsChange!: EventEmitter<FilterMenuChangeDetail>;\n /** Requests that the consumer clear every filter value. */\n @Event() dsClear!: EventEmitter<void>;\n /** Requests a controlled active-category replacement. */\n @Event() dsActiveFilterChange!: EventEmitter<string>;\n /** Requests that the controlled popup close. */\n @Event() dsClose!: EventEmitter<void>;\n /** Emitted whenever internal select-trigger activation changes popup visibility. */\n @Event() dsOpenChange!: EventEmitter<boolean>;\n /** Emitted after exit motion and rendered popup removal complete. */\n @Event() dsAfterClose!: EventEmitter<void>;\n\n private readonly generatedId = `ds-filter-menu-${++filterMenuId}`;\n private outsideHandler: ((event: MouseEvent) => void) | null = null;\n private triggerElement: HTMLButtonElement | null = null;\n private pendingInitialFocusVisible = false;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private captionCompactDisconnect: (() => void) | undefined;\n private hasLoaded = false;\n private closingSnapshot: {\n filters: FilterMenuFilter[];\n values: FilterMenuValues;\n activeFilterId: string | undefined;\n } | null = null;\n\n private readonly position = new AnchoredPositionController({\n getAnchor: () => this.resolvedAnchor,\n getPopup: () => this.el.querySelector<HTMLElement>('.filter-menu-popup'),\n measure: (anchor, popup) => {\n if (!this.open) return null;\n const anchorRect = anchor.getBoundingClientRect();\n const sectionInsetPx = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n return {\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidth,\n popupHeight: popup.offsetHeight || this.popupFallbackHeight,\n side: this.side,\n align: this.align,\n sideOffsetPx: resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050),\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: resolveCssLengthPx(this.alignOffset, 0),\n sectionInsetPx,\n }),\n viewportPadPx: this.viewportPad,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n };\n },\n apply: ({ x, y }) => {\n this.pos = { x, y };\n },\n onReady: () => {\n this.positionReady = true;\n },\n liveUpdate: 'double-frame',\n observeResize: true,\n topLayer: true,\n });\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.syncCaptionCompactObserver();\n if (this.open) this.onOpenChange(true);\n }\n\n connectedCallback() {\n if (!this.hasLoaded) return;\n this.syncCaptionCompactObserver();\n if (this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.disconnectCaptionCompactObserver();\n this.position.unobserve();\n this.teardownListeners();\n }\n\n @Watch('collapseLabel')\n onCollapseLabelChange() {\n this.syncCaptionCompactObserver();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardownListeners();\n this.closingSnapshot = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.focusRingVisible = this.pendingInitialFocusVisible || this.initialFocusVisible;\n this.setupListeners();\n this.position.schedule(() => this.focusInitialCategory());\n return;\n }\n\n if (!this.shouldRender) return;\n this.position.cancel();\n this.captureClosingSnapshot();\n this.closing = true;\n this.teardownListeners();\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort2,\n TOKEN_DEFAULTS.animationDurationShort3\n );\n if (duration <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => this.finishClose(), duration);\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n @Watch('side')\n @Watch('align')\n @Watch('sideOffset')\n @Watch('alignOffset')\n @Watch('menuWidth')\n onPositionInputChange() {\n if (this.open) this.position.schedule();\n }\n\n @Listen('keydown')\n handleKeyDown(event: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.close();\n return;\n }\n if (event.key !== 'Tab' || !this.tabLeavesPopup(event)) return;\n event.preventDefault();\n this.close(false);\n this.moveFocusAfterTab(event.shiftKey);\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return this.triggerElement;\n }\n\n private get usesInternalTrigger(): boolean {\n return !this.anchor && !this.anchorId;\n }\n\n private get anchorFocusTarget(): HTMLElement | null {\n const anchor = this.resolvedAnchor;\n if (!anchor) return null;\n if (anchor.matches(FILTER_MENU_FOCUSABLE_SELECTOR)) return anchor;\n return anchor.querySelector<HTMLElement>(FILTER_MENU_FOCUSABLE_SELECTOR) ?? anchor;\n }\n\n private get viewportPad(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get popupFallbackWidth(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthLg, TOKEN_DEFAULTS.menuWidthLg);\n }\n\n private get popupFallbackHeight(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private setupListeners() {\n this.outsideHandler = event => {\n const target = event.target as Node;\n const popup = this.el.querySelector('.filter-menu-popup');\n if (popup?.contains(target) || this.resolvedAnchor?.contains(target)) return;\n this.close();\n };\n document.addEventListener('mousedown', this.outsideHandler, true);\n this.position.observe();\n }\n\n private teardownListeners() {\n this.position.unobserve();\n if (this.outsideHandler) {\n document.removeEventListener('mousedown', this.outsideHandler, true);\n this.outsideHandler = null;\n }\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n this.captureClosingSnapshot();\n if (restoreFocus) this.focusAnchor();\n this.dsOpenChange.emit(false);\n this.dsClose.emit();\n this.open = false;\n this.onOpenChange(false);\n }\n\n private toggleFromTrigger(event: MouseEvent) {\n if (this.open) {\n this.close();\n return;\n }\n this.pendingInitialFocusVisible = event.detail === 0;\n this.open = true;\n this.dsOpenChange.emit(true);\n this.onOpenChange(true);\n }\n\n private handleTriggerKeyDown(event: KeyboardEvent) {\n if (event.key !== 'ArrowDown' || this.open) return;\n event.preventDefault();\n this.pendingInitialFocusVisible = true;\n this.open = true;\n this.dsOpenChange.emit(true);\n this.onOpenChange(true);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.filter-menu-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.closingSnapshot = null;\n this.closeTimer = null;\n this.dsAfterClose.emit();\n }\n\n private captureClosingSnapshot() {\n if (this.closingSnapshot) return;\n this.closingSnapshot = {\n filters: this.filters.map(filter => ({\n ...filter,\n options: filter.options?.map(option => ({ ...option })),\n })),\n values: Object.fromEntries(\n Object.entries(this.values).map(([key, value]) => [\n key,\n Array.isArray(value) ? [...value] : value,\n ])\n ),\n activeFilterId: this.activeFilterId,\n };\n }\n\n private focusAnchor() {\n const anchor = this.resolvedAnchor as\n (HTMLElement & { setFocus?: () => Promise<void> | void }) | null;\n if (anchor?.setFocus) {\n void anchor.setFocus();\n return;\n }\n this.anchorFocusTarget?.focus();\n }\n\n private focusInitialCategory() {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.filter-menu__category[tabindex=\"0\"]')?.focus();\n this.pendingInitialFocusVisible = false;\n });\n }\n\n private tabLeavesPopup(event: KeyboardEvent): boolean {\n const popup = this.el.querySelector<HTMLElement>('.filter-menu-popup');\n if (!popup) return true;\n const focusables = Array.from(\n popup.querySelectorAll<HTMLElement>(FILTER_MENU_FOCUSABLE_SELECTOR)\n ).filter(element => element.getClientRects().length > 0);\n const index = focusables.indexOf(event.target as HTMLElement);\n if (index < 0) return true;\n return event.shiftKey ? index === 0 : index === focusables.length - 1;\n }\n\n private moveFocusAfterTab(backwards: boolean) {\n const anchor = this.anchorFocusTarget;\n if (!anchor) return;\n if (backwards) {\n anchor.focus();\n return;\n }\n const candidates = Array.from(\n document.querySelectorAll<HTMLElement>(FILTER_MENU_FOCUSABLE_SELECTOR)\n ).filter(\n element =>\n !element.closest('.filter-menu-popup') &&\n !element.closest('[inert]') &&\n element.getClientRects().length > 0\n );\n const index = candidates.indexOf(anchor);\n (candidates[index + 1] ?? anchor).focus();\n }\n\n private selectedFilter(filters: FilterMenuFilter[], activeFilterId: string | undefined) {\n return filters.find(filter => filter.id === activeFilterId) ?? filters[0];\n }\n\n private selectedCount(filter: FilterMenuFilter, values: FilterMenuValues): number {\n const value = values[filter.id];\n if (filter.kind === 'multiple') return Array.isArray(value) ? value.length : 0;\n if (filter.kind === 'boolean') return value === true ? 1 : 0;\n return typeof value === 'string' && value.length > 0 ? 1 : 0;\n }\n\n private totalSelected(filters: FilterMenuFilter[], values: FilterMenuValues): number {\n return filters.reduce((total, filter) => total + this.selectedCount(filter, values), 0);\n }\n\n private activeFilterCount(filters: FilterMenuFilter[], values: FilterMenuValues): number {\n return filters.filter(filter => this.selectedCount(filter, values) > 0).length;\n }\n\n private selectCategory(filterId: string, focus = false) {\n this.activeOptionIndex = 0;\n this.dsActiveFilterChange.emit(filterId);\n if (!focus) return;\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>(`[data-filter-category=\"${filterId}\"]`)?.focus();\n });\n }\n\n private handleCategoryKeyDown(\n event: KeyboardEvent,\n filters: FilterMenuFilter[],\n filterId: string\n ) {\n const currentIndex = filters.findIndex(filter => filter.id === filterId);\n let nextIndex: number;\n if (event.key === 'ArrowDown') nextIndex = (currentIndex + 1) % filters.length;\n else if (event.key === 'ArrowUp')\n nextIndex = (currentIndex - 1 + filters.length) % filters.length;\n else if (event.key === 'Home') nextIndex = 0;\n else if (event.key === 'End') nextIndex = filters.length - 1;\n else if (event.key === 'ArrowRight') {\n event.preventDefault();\n requestAnimationFrame(() => {\n this.el\n .querySelector<HTMLElement>(\n '.filter-menu__options [role=\"option\"], .filter-menu__options input'\n )\n ?.focus();\n });\n return;\n } else return;\n\n event.preventDefault();\n this.focusRingVisible = true;\n this.selectCategory(filters[nextIndex].id, true);\n }\n\n private handlePanelKeyDown(event: KeyboardEvent, filterId: string) {\n if (event.key !== 'ArrowLeft') return;\n event.preventDefault();\n this.el.querySelector<HTMLElement>(`[data-filter-category=\"${filterId}\"]`)?.focus();\n }\n\n private handleOptionKeyDown(\n event: KeyboardEvent,\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n index: number,\n values: FilterMenuValues\n ) {\n const options = filter.options ?? [];\n if (event.key === 'ArrowLeft') {\n this.handlePanelKeyDown(event, filter.id);\n return;\n }\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.selectOption(filter, option, values);\n return;\n }\n if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;\n event.preventDefault();\n const enabled = options\n .map((candidate, optionIndex) => ({ candidate, optionIndex }))\n .filter(({ candidate }) => !candidate.isInactive)\n .map(({ optionIndex }) => optionIndex);\n if (!enabled.length) return;\n const current = enabled.indexOf(index);\n let next: number;\n if (event.key === 'Home') next = enabled[0];\n else if (event.key === 'End') next = enabled[enabled.length - 1];\n else {\n const direction = event.key === 'ArrowDown' ? 1 : -1;\n const start = current >= 0 ? current : direction === 1 ? -1 : 0;\n next = enabled[(start + direction + enabled.length) % enabled.length];\n }\n this.activeOptionIndex = next;\n this.focusRingVisible = true;\n requestAnimationFrame(() => {\n this.el\n .querySelector<HTMLElement>(`#${this.generatedId}-${filter.id}-option-${next}`)\n ?.focus();\n });\n }\n\n private toggleMultiple(\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n checked: boolean,\n values: FilterMenuValues\n ) {\n if (option.isInactive) return;\n const current = Array.isArray(values[filter.id]) ? (values[filter.id] as string[]) : [];\n const selected = new Set(current);\n if (checked) selected.add(option.value);\n else selected.delete(option.value);\n const ordered = (filter.options ?? [])\n .map(candidate => candidate.value)\n .filter(value => selected.has(value));\n this.dsChange.emit({ filterId: filter.id, value: ordered });\n }\n\n private selectOption(\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n values: FilterMenuValues\n ) {\n if (option.isInactive) return;\n if (filter.kind === 'single') {\n this.dsChange.emit({ filterId: filter.id, value: option.value });\n return;\n }\n const selected = Array.isArray(values[filter.id])\n ? (values[filter.id] as string[]).includes(option.value)\n : false;\n this.toggleMultiple(filter, option, !selected, values);\n }\n\n private renderOptions(filter: FilterMenuFilter, values: FilterMenuValues) {\n const value = values[filter.id];\n if (filter.kind === 'multiple' || filter.kind === 'single') {\n const selected = Array.isArray(value) ? value : [];\n const options = filter.options ?? [];\n const usesSubtext = choiceListUsesSubtext(\n options.map(option => ({\n label: option.label,\n value: option.value,\n subtext: option.description,\n }))\n );\n return options.map((option, index) => {\n const isSelected =\n filter.kind === 'multiple' ? selected.includes(option.value) : value === option.value;\n return (\n <ChoiceOptionRow\n size=\"md\"\n id={`${this.generatedId}-${filter.id}-option-${index}`}\n option={{\n label: option.label,\n value: option.value,\n subtext: option.description,\n isInactive: option.isInactive,\n }}\n selected={isSelected}\n active={index === this.activeOptionIndex}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={usesSubtext}\n tabIndex={index === this.activeOptionIndex ? 0 : -1}\n leading={\n filter.kind === 'multiple' ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox label=\"\" size=\"md\" checked={isSelected} presentation />\n </span>\n ) : undefined\n }\n onFocus={() => {\n this.activeOptionIndex = index;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n this.handleOptionKeyDown(event, filter, option, index, values)\n }\n onHover={() => {\n this.focusRingVisible = false;\n this.activeOptionIndex = index;\n }}\n onSelect={() => this.selectOption(filter, option, values)}\n />\n );\n });\n }\n\n if (filter.kind === 'boolean') {\n const option: FilterMenuOption = {\n label: filter.fieldLabel ?? filter.label,\n value: 'true',\n description: filter.description,\n };\n return (\n <ChoiceOptionRow\n size=\"md\"\n id={`${this.generatedId}-${filter.id}-option-0`}\n option={{\n label: option.label,\n value: option.value,\n subtext: option.description,\n }}\n selected={value === true}\n active\n focusRingVisible={this.focusRingVisible}\n usesSubtext={Boolean(option.description)}\n tabIndex={0}\n leading={\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox label=\"\" size=\"md\" checked={value === true} presentation />\n </span>\n }\n onKeyDown={(event: KeyboardEvent) => {\n if (event.key === 'ArrowLeft') this.handlePanelKeyDown(event, filter.id);\n else if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.dsChange.emit({ filterId: filter.id, value: value !== true });\n }\n }}\n onHover={() => {\n this.focusRingVisible = false;\n }}\n onSelect={() => this.dsChange.emit({ filterId: filter.id, value: value !== true })}\n />\n );\n }\n\n const dateValue = typeof value === 'string' ? value : '';\n const inputId = `${this.generatedId}-${filter.id}-input`;\n return (\n <div class=\"filter-menu__date-field\">\n <label htmlFor={inputId}>\n <ds-text as=\"span\" variant=\"text-body-small\" emphasis color=\"primary\">\n {filter.fieldLabel ?? 'Event date'}\n </ds-text>\n </label>\n <div class=\"filter-menu__date-control ds-control-frame ds-control--md\">\n <input\n id={inputId}\n class=\"filter-menu__date-input\"\n type=\"date\"\n value={dateValue}\n onInput={(event: Event) =>\n this.dsChange.emit({\n filterId: filter.id,\n value: (event.target as HTMLInputElement).value,\n })\n }\n />\n {dateValue ? (\n <ds-button-unfilled\n variant=\"icon\"\n icon=\"CrossCircle\"\n size=\"sm\"\n isInset\n hasBorder={false}\n rounded\n ariaLabel={`${this.clearLabel} ${filter.fieldLabel ?? filter.label}`}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsChange.emit({ filterId: filter.id, value: '' });\n }}\n />\n ) : null}\n </div>\n </div>\n );\n }\n\n private get captionIconOnly(): boolean {\n return (\n this.collapseLabel &&\n this.captionCompact &&\n this.usesInternalTrigger &&\n Boolean(this.icon)\n );\n }\n\n private syncCaptionCompactObserver(): void {\n this.disconnectCaptionCompactObserver();\n if (!this.collapseLabel) {\n if (this.captionCompact) this.captionCompact = false;\n return;\n }\n this.captionCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.captionCompact !== compact) this.captionCompact = compact;\n });\n }\n\n private disconnectCaptionCompactObserver(): void {\n this.captionCompactDisconnect?.();\n this.captionCompactDisconnect = undefined;\n }\n\n render() {\n const state =\n this.closing && this.closingSnapshot\n ? this.closingSnapshot\n : {\n filters: this.filters,\n values: this.values,\n activeFilterId: this.activeFilterId,\n };\n const activeFilter = this.selectedFilter(state.filters, state.activeFilterId);\n const totalSelected = this.totalSelected(state.filters, state.values);\n const activeFilterCount = this.activeFilterCount(state.filters, state.values);\n const hasActiveFilters = activeFilterCount > 0;\n const label = `${this.triggerLabel}${hasActiveFilters ? ` · ${activeFilterCount}` : ''}`;\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n const popupId = `${this.generatedId}-popup`;\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n width: this.menuWidth,\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n return (\n <Host\n class={{\n 'filter-menu-host': true,\n 'filter-menu-host--external-trigger': !this.usesInternalTrigger,\n 'ds-select-trigger-host': this.usesInternalTrigger,\n [`ds-control--${this.size}`]: true,\n 'ds-table-caption-control': this.collapseLabel,\n 'ds-table-caption-control--compact': this.captionIconOnly,\n ...controlWidthClass(this.width),\n }}\n >\n {this.usesInternalTrigger ? (\n <ds-tooltip\n label={this.captionIconOnly ? label : ''}\n side=\"top\"\n size=\"sm\"\n >\n <button\n ref={element => {\n this.triggerElement = (element as HTMLButtonElement) ?? null;\n }}\n id={this.inputId ?? `${this.generatedId}-trigger`}\n type=\"button\"\n class={{\n trigger: true,\n 'ds-control-frame': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': this.activeFill && hasActiveFilters,\n 'trigger--expanded': this.open || this.closing,\n 'trigger--bordered': this.hasBorder,\n 'trigger--has-value': hasActiveFilters,\n [`ds-control--${this.size}`]: true,\n }}\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n aria-expanded={String(this.open)}\n aria-controls={this.open ? popupId : undefined}\n aria-label={this.ariaLabel ?? this.menuLabel}\n onClick={(event: MouseEvent) => this.toggleFromTrigger(event)}\n onKeyDown={(event: KeyboardEvent) => this.handleTriggerKeyDown(event)}\n >\n {this.icon ? (\n <span\n class=\"trigger__prefix ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n </span>\n ) : null}\n {this.captionIconOnly ? null : (\n <ds-text\n class=\"trigger__label-box trigger__label trigger__label-content ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant={textVariant}\n color=\"inherit\"\n lineTruncation={1}\n >\n {label}\n </ds-text>\n )}\n {this.captionIconOnly ? null : (\n <span\n class=\"trigger__chevron ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name=\"ChevronDown\" size={iconSize} color=\"inherit\" />\n </span>\n )}\n </button>\n </ds-tooltip>\n ) : null}\n\n {this.shouldRender ? (\n <div\n id={popupId}\n popover=\"manual\"\n class={{\n 'filter-menu-popup': true,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={popupStyle}\n role=\"dialog\"\n aria-label={this.menuLabel}\n >\n {activeFilter ? (\n <div class=\"filter-menu\">\n <div class=\"filter-menu__body\">\n <div\n class=\"filter-menu__categories ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n role=\"tablist\"\n aria-label={this.categoriesLabel}\n aria-orientation=\"vertical\"\n >\n {state.filters.map(filter => {\n const active = filter.id === activeFilter.id;\n const count = this.selectedCount(filter, state.values);\n return (\n <div\n key={filter.id}\n class={{\n 'filter-menu__category-section': true,\n 'ds-choice-section--divided': Boolean(filter.divider),\n }}\n >\n <button\n id={`${this.generatedId}-${filter.id}-tab`}\n type=\"button\"\n role=\"tab\"\n data-filter-category={filter.id}\n class={{\n 'filter-menu__category': true,\n 'filter-menu__category--active': active,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': active && this.focusRingVisible,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': active,\n }}\n aria-selected={String(active)}\n aria-controls={`${this.generatedId}-panel`}\n tabIndex={active ? 0 : -1}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.selectCategory(filter.id)}\n onKeyDown={(event: KeyboardEvent) =>\n this.handleCategoryKeyDown(event, state.filters, filter.id)\n }\n >\n <ds-text\n class=\"ds-choice-item__content ds-choice-item__label ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={active ? 'primary' : 'secondary'}\n >\n {filter.label}\n </ds-text>\n {count > 0 ? (\n <ds-tag\n class=\"ds-choice-item__tag ds-interaction-fill__content\"\n label={String(count)}\n size=\"sm\"\n intent=\"brand\"\n contrast=\"bold\"\n isInset\n rounded\n />\n ) : null}\n </button>\n </div>\n );\n })}\n </div>\n\n <div\n id={`${this.generatedId}-panel`}\n class=\"filter-menu__options\"\n role=\"tabpanel\"\n aria-labelledby={`${this.generatedId}-${activeFilter.id}-tab`}\n >\n <div\n key={activeFilter.id}\n class=\"filter-menu__option-list ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n role=\"listbox\"\n aria-label={activeFilter.label}\n aria-multiselectable={activeFilter.kind === 'multiple' ? 'true' : undefined}\n >\n {this.renderOptions(activeFilter, state.values)}\n </div>\n </div>\n </div>\n\n {totalSelected > 0 ? (\n <div class=\"filter-menu__footer ds-choice-footer\">\n <div class=\"ds-choice-footer__content ds-control--md\">\n <ds-text\n class=\"ds-choice-footer__summary\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-live=\"polite\"\n >\n {totalSelected} selected\n </ds-text>\n <button\n class=\"filter-menu__clear ds-choice-footer__clear ds-text-action\"\n type=\"button\"\n onClick={() => this.dsClear.emit()}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {this.clearLabel}\n </ds-text>\n </button>\n </div>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n ) : null}\n </Host>\n );\n }\n}\n",
679
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Prop,\n State,\n type VNode,\n Watch,\n} from '@stencil/core';\nimport {\n CONTROL_TEXT_VARIANT,\n controlWidthClass,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ControlWidth,\n} from '../../utils';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport type { AnchoredAlign, AnchoredSide } from '../../utils/anchored-position';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\nimport { ChoiceOptionRow } from '../../utils/choice-list-parts';\nimport { choiceListUsesSubtext } from '../../utils/choice-list';\n\nexport type FilterMenuFilterKind = 'single' | 'multiple' | 'boolean' | 'date';\nexport type FilterMenuSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type FilterMenuWidth = ControlWidth;\n\nexport interface FilterMenuOption {\n label: string;\n value: string;\n description?: string;\n isInactive?: boolean;\n}\n\nexport interface FilterMenuFilter {\n id: string;\n label: string;\n kind: FilterMenuFilterKind;\n options?: FilterMenuOption[];\n description?: string;\n fieldLabel?: string;\n /** Draw a standard menu-section divider after this category. */\n divider?: boolean;\n}\n\nexport type FilterMenuValue = string[] | boolean | string;\nexport type FilterMenuValues = Record<string, FilterMenuValue | undefined>;\n\nexport interface FilterMenuChangeDetail {\n filterId: string;\n value: FilterMenuValue;\n}\n\nlet filterMenuId = 0;\n\nconst ICON_SIZE: Record<FilterMenuSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\n@Component({\n tag: 'ds-filter-menu',\n styleUrl: 'FilterMenu.css',\n scoped: true,\n})\nexport class FilterMenu {\n @Element() el!: HTMLElement;\n\n /** Controlled popup visibility. */\n @Prop({ mutable: true }) open: boolean = false;\n /** Select trigger text. The selected count is appended automatically. */\n @Prop() triggerLabel: string = 'Filters';\n /** Optional select trigger prefix icon name. */\n @Prop() icon: string | undefined = 'Filters';\n /** Select trigger density. */\n @Prop() size: FilterMenuSize = 'md';\n /** Select trigger width fit. */\n @Prop() width: FilterMenuWidth = 'hug';\n /** Show the surface-aware inset border around the select trigger. */\n @Prop() hasBorder: boolean = true;\n /** Show selected interaction fill when one or more criteria are active. */\n @Prop() activeFill: boolean = false;\n /**\n * Opt into table-caption icon-only chrome below 900px. The trigger omits its\n * visible label and chevron; keep an accessible name via aria-label.\n */\n @Prop({ reflect: true }) collapseLabel: boolean = false;\n /** ID applied to the internal select trigger. */\n @Prop() inputId: string | undefined;\n /** Direct accessible name for the internal select trigger. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Product-owned filter categories and option definitions. */\n @Prop() filters: FilterMenuFilter[] = [];\n /** Controlled values keyed by filter id. */\n @Prop() values: FilterMenuValues = {};\n /** Controlled category shown in the option pane. */\n @Prop() activeFilterId: string | undefined;\n /** External trigger element to position against. */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element used for positioning and focus return. */\n @Prop() anchorId: string | undefined;\n /** Explicit collision owner; otherwise the nearest data-ds-overlay-boundary ancestor is used. */\n @Prop() boundary: HTMLElement | undefined;\n /** Preferred popup side; collision handling may flip it. */\n @Prop() side: AnchoredSide = 'bottom';\n /** Cross-axis alignment against the trigger. */\n @Prop() align: AnchoredAlign = 'end';\n /** Gap between trigger and popup — number in px or a TokoMo length. */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number in px or a TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n /** Popup width. It remains clamped to its collision boundary by the component recipe. */\n @Prop() menuWidth: string = TOKEN_CSS_LENGTHS.menuWidthLg;\n /** Accessible name for the non-modal filter dialog. */\n @Prop() menuLabel: string = 'Filters';\n /** Accessible name for the category tab list. */\n @Prop() categoriesLabel: string = 'Filter categories';\n /** Footer action and date-clear accessible label. */\n @Prop() clearLabel: string = 'Clear';\n /** Show a visible focus ring on initial entry after keyboard activation. */\n @Prop() initialFocusVisible: boolean = false;\n\n @State() private shouldRender = false;\n @State() private closing = false;\n @State() private positionReady = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusRingVisible = false;\n @State() private activeOptionIndex = 0;\n @State() private captionCompact = false;\n\n /** Requests a controlled value replacement without closing the popup. */\n @Event() dsChange!: EventEmitter<FilterMenuChangeDetail>;\n /** Requests that the consumer clear every filter value. */\n @Event() dsClear!: EventEmitter<void>;\n /** Requests a controlled active-category replacement. */\n @Event() dsActiveFilterChange!: EventEmitter<string>;\n /** Requests that the controlled popup close. */\n @Event() dsClose!: EventEmitter<void>;\n /** Emitted whenever internal select-trigger activation changes popup visibility. */\n @Event() dsOpenChange!: EventEmitter<boolean>;\n /** Emitted after exit motion and rendered popup removal complete. */\n @Event() dsAfterClose!: EventEmitter<void>;\n\n private readonly generatedId = `ds-filter-menu-${++filterMenuId}`;\n private triggerElement: HTMLButtonElement | null = null;\n private pendingInitialFocusVisible = false;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private captionCompactDisconnect: (() => void) | undefined;\n private hasLoaded = false;\n private closingSnapshot: {\n filters: FilterMenuFilter[];\n values: FilterMenuValues;\n activeFilterId: string | undefined;\n } | null = null;\n\n private readonly position = new AnchoredPositionController({\n getAnchor: () => this.resolvedAnchor,\n getPopup: () => this.el.querySelector<HTMLElement>('.filter-menu-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchor, popup) => {\n if (!this.open) return null;\n const anchorRect = anchor.getBoundingClientRect();\n const sectionInsetPx = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n return {\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidth,\n popupHeight: popup.offsetHeight || this.popupFallbackHeight,\n side: this.side,\n align: this.align,\n sideOffsetPx: resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050),\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: resolveCssLengthPx(this.alignOffset, 0),\n sectionInsetPx,\n }),\n viewportPadPx: this.viewportPad,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchor, this.boundary),\n };\n },\n apply: ({ x, y }) => {\n this.pos = { x, y };\n },\n onReady: () => {\n this.positionReady = true;\n },\n liveUpdate: 'double-frame',\n observeResize: true,\n topLayer: true,\n });\n private readonly interaction = new AnchoredOverlayInteractionController({\n getAnchor: () => this.resolvedAnchor,\n getPopup: () => this.el.querySelector<HTMLElement>('.filter-menu-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.close(),\n });\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.syncCaptionCompactObserver();\n if (this.open) this.onOpenChange(true);\n }\n\n connectedCallback() {\n if (!this.hasLoaded) return;\n this.syncCaptionCompactObserver();\n if (this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.disconnectCaptionCompactObserver();\n this.position.unobserve();\n this.teardownListeners();\n }\n\n @Watch('collapseLabel')\n onCollapseLabelChange() {\n this.syncCaptionCompactObserver();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardownListeners();\n this.closingSnapshot = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.focusRingVisible = this.pendingInitialFocusVisible || this.initialFocusVisible;\n this.setupListeners();\n this.position.schedule(() => this.focusInitialCategory());\n return;\n }\n\n if (!this.shouldRender) return;\n this.position.cancel();\n this.captureClosingSnapshot();\n this.closing = true;\n this.teardownListeners();\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort2,\n TOKEN_DEFAULTS.animationDurationShort3\n );\n if (duration <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => this.finishClose(), duration);\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n @Watch('boundary')\n @Watch('side')\n @Watch('align')\n @Watch('sideOffset')\n @Watch('alignOffset')\n @Watch('menuWidth')\n onPositionInputChange() {\n if (this.open) this.position.schedule();\n }\n\n @Listen('keydown')\n handleKeyDown(event: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.close();\n return;\n }\n if (event.key !== 'Tab' || !this.tabLeavesPopup(event)) return;\n event.preventDefault();\n this.close(false);\n this.interaction.moveFocusAfterTab(event.shiftKey);\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return this.triggerElement;\n }\n\n private get usesInternalTrigger(): boolean {\n return !this.anchor && !this.anchorId;\n }\n\n private get viewportPad(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get popupFallbackWidth(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthLg, TOKEN_DEFAULTS.menuWidthLg);\n }\n\n private get popupFallbackHeight(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private setupListeners() {\n this.interaction.connect();\n this.position.observe();\n }\n\n private teardownListeners() {\n this.position.unobserve();\n this.interaction.disconnect();\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n this.captureClosingSnapshot();\n if (restoreFocus) this.interaction.focusAnchor();\n this.dsOpenChange.emit(false);\n this.dsClose.emit();\n this.open = false;\n this.onOpenChange(false);\n }\n\n private toggleFromTrigger(event: MouseEvent) {\n if (this.open) {\n this.close();\n return;\n }\n this.pendingInitialFocusVisible = event.detail === 0;\n this.open = true;\n this.dsOpenChange.emit(true);\n this.onOpenChange(true);\n }\n\n private handleTriggerKeyDown(event: KeyboardEvent) {\n if (event.key !== 'ArrowDown' || this.open) return;\n event.preventDefault();\n this.pendingInitialFocusVisible = true;\n this.open = true;\n this.dsOpenChange.emit(true);\n this.onOpenChange(true);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.filter-menu-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.closingSnapshot = null;\n this.closeTimer = null;\n this.dsAfterClose.emit();\n }\n\n private captureClosingSnapshot() {\n if (this.closingSnapshot) return;\n this.closingSnapshot = {\n filters: this.filters.map(filter => ({\n ...filter,\n options: filter.options?.map(option => ({ ...option })),\n })),\n values: Object.fromEntries(\n Object.entries(this.values).map(([key, value]) => [\n key,\n Array.isArray(value) ? [...value] : value,\n ])\n ),\n activeFilterId: this.activeFilterId,\n };\n }\n\n private focusInitialCategory() {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.filter-menu__category[tabindex=\"0\"]')?.focus();\n this.pendingInitialFocusVisible = false;\n });\n }\n\n private tabLeavesPopup(event: KeyboardEvent): boolean {\n return this.interaction.tabLeavesPopup(event);\n }\n\n private selectedFilter(filters: FilterMenuFilter[], activeFilterId: string | undefined) {\n return filters.find(filter => filter.id === activeFilterId) ?? filters[0];\n }\n\n private selectedCount(filter: FilterMenuFilter, values: FilterMenuValues): number {\n const value = values[filter.id];\n if (filter.kind === 'multiple') return Array.isArray(value) ? value.length : 0;\n if (filter.kind === 'boolean') return value === true ? 1 : 0;\n return typeof value === 'string' && value.length > 0 ? 1 : 0;\n }\n\n private totalSelected(filters: FilterMenuFilter[], values: FilterMenuValues): number {\n return filters.reduce((total, filter) => total + this.selectedCount(filter, values), 0);\n }\n\n private activeFilterCount(filters: FilterMenuFilter[], values: FilterMenuValues): number {\n return filters.filter(filter => this.selectedCount(filter, values) > 0).length;\n }\n\n private selectCategory(filterId: string, focus = false) {\n this.activeOptionIndex = 0;\n this.dsActiveFilterChange.emit(filterId);\n if (!focus) return;\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>(`[data-filter-category=\"${filterId}\"]`)?.focus();\n });\n }\n\n private handleCategoryKeyDown(\n event: KeyboardEvent,\n filters: FilterMenuFilter[],\n filterId: string\n ) {\n const currentIndex = filters.findIndex(filter => filter.id === filterId);\n let nextIndex: number;\n if (event.key === 'ArrowDown') nextIndex = (currentIndex + 1) % filters.length;\n else if (event.key === 'ArrowUp')\n nextIndex = (currentIndex - 1 + filters.length) % filters.length;\n else if (event.key === 'Home') nextIndex = 0;\n else if (event.key === 'End') nextIndex = filters.length - 1;\n else if (event.key === 'ArrowRight') {\n event.preventDefault();\n requestAnimationFrame(() => {\n this.el\n .querySelector<HTMLElement>(\n '.filter-menu__options [role=\"option\"], .filter-menu__options input'\n )\n ?.focus();\n });\n return;\n } else return;\n\n event.preventDefault();\n this.focusRingVisible = true;\n this.selectCategory(filters[nextIndex].id, true);\n }\n\n private handlePanelKeyDown(event: KeyboardEvent, filterId: string) {\n if (event.key !== 'ArrowLeft') return;\n event.preventDefault();\n this.el.querySelector<HTMLElement>(`[data-filter-category=\"${filterId}\"]`)?.focus();\n }\n\n private handleOptionKeyDown(\n event: KeyboardEvent,\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n index: number,\n values: FilterMenuValues\n ) {\n const options = filter.options ?? [];\n if (event.key === 'ArrowLeft') {\n this.handlePanelKeyDown(event, filter.id);\n return;\n }\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.selectOption(filter, option, values);\n return;\n }\n if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;\n event.preventDefault();\n const enabled = options\n .map((candidate, optionIndex) => ({ candidate, optionIndex }))\n .filter(({ candidate }) => !candidate.isInactive)\n .map(({ optionIndex }) => optionIndex);\n if (!enabled.length) return;\n const current = enabled.indexOf(index);\n let next: number;\n if (event.key === 'Home') next = enabled[0];\n else if (event.key === 'End') next = enabled[enabled.length - 1];\n else {\n const direction = event.key === 'ArrowDown' ? 1 : -1;\n const start = current >= 0 ? current : direction === 1 ? -1 : 0;\n next = enabled[(start + direction + enabled.length) % enabled.length];\n }\n this.activeOptionIndex = next;\n this.focusRingVisible = true;\n requestAnimationFrame(() => {\n this.el\n .querySelector<HTMLElement>(`#${this.generatedId}-${filter.id}-option-${next}`)\n ?.focus();\n });\n }\n\n private toggleMultiple(\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n checked: boolean,\n values: FilterMenuValues\n ) {\n if (option.isInactive) return;\n const current = Array.isArray(values[filter.id]) ? (values[filter.id] as string[]) : [];\n const selected = new Set(current);\n if (checked) selected.add(option.value);\n else selected.delete(option.value);\n const ordered = (filter.options ?? [])\n .map(candidate => candidate.value)\n .filter(value => selected.has(value));\n this.dsChange.emit({ filterId: filter.id, value: ordered });\n }\n\n private selectOption(\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n values: FilterMenuValues\n ) {\n if (option.isInactive) return;\n if (filter.kind === 'single') {\n this.dsChange.emit({ filterId: filter.id, value: option.value });\n return;\n }\n const selected = Array.isArray(values[filter.id])\n ? (values[filter.id] as string[]).includes(option.value)\n : false;\n this.toggleMultiple(filter, option, !selected, values);\n }\n\n private renderOptions(filter: FilterMenuFilter, values: FilterMenuValues) {\n const value = values[filter.id];\n if (filter.kind === 'multiple' || filter.kind === 'single') {\n const selected = Array.isArray(value) ? value : [];\n const options = filter.options ?? [];\n const usesSubtext = choiceListUsesSubtext(\n options.map(option => ({\n label: option.label,\n value: option.value,\n subtext: option.description,\n }))\n );\n return options.map((option, index) => {\n const isSelected =\n filter.kind === 'multiple' ? selected.includes(option.value) : value === option.value;\n return (\n <ChoiceOptionRow\n size=\"md\"\n id={`${this.generatedId}-${filter.id}-option-${index}`}\n option={{\n label: option.label,\n value: option.value,\n subtext: option.description,\n isInactive: option.isInactive,\n }}\n selected={isSelected}\n active={index === this.activeOptionIndex}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={usesSubtext}\n tabIndex={index === this.activeOptionIndex ? 0 : -1}\n leading={\n filter.kind === 'multiple' ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox label=\"\" size=\"md\" checked={isSelected} presentation />\n </span>\n ) : undefined\n }\n onFocus={() => {\n this.activeOptionIndex = index;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n this.handleOptionKeyDown(event, filter, option, index, values)\n }\n onHover={() => {\n this.focusRingVisible = false;\n this.activeOptionIndex = index;\n }}\n onSelect={() => this.selectOption(filter, option, values)}\n />\n );\n });\n }\n\n if (filter.kind === 'boolean') {\n const option: FilterMenuOption = {\n label: filter.fieldLabel ?? filter.label,\n value: 'true',\n description: filter.description,\n };\n return (\n <ChoiceOptionRow\n size=\"md\"\n id={`${this.generatedId}-${filter.id}-option-0`}\n option={{\n label: option.label,\n value: option.value,\n subtext: option.description,\n }}\n selected={value === true}\n active\n focusRingVisible={this.focusRingVisible}\n usesSubtext={Boolean(option.description)}\n tabIndex={0}\n leading={\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox label=\"\" size=\"md\" checked={value === true} presentation />\n </span>\n }\n onKeyDown={(event: KeyboardEvent) => {\n if (event.key === 'ArrowLeft') this.handlePanelKeyDown(event, filter.id);\n else if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.dsChange.emit({ filterId: filter.id, value: value !== true });\n }\n }}\n onHover={() => {\n this.focusRingVisible = false;\n }}\n onSelect={() => this.dsChange.emit({ filterId: filter.id, value: value !== true })}\n />\n );\n }\n\n const dateValue = typeof value === 'string' ? value : '';\n const inputId = `${this.generatedId}-${filter.id}-input`;\n return (\n <div class=\"filter-menu__date-field\">\n <label htmlFor={inputId}>\n <ds-text as=\"span\" variant=\"text-body-small\" emphasis color=\"primary\">\n {filter.fieldLabel ?? 'Event date'}\n </ds-text>\n </label>\n <div class=\"filter-menu__date-control ds-control-frame ds-control--md\">\n <input\n id={inputId}\n class=\"filter-menu__date-input\"\n type=\"date\"\n value={dateValue}\n onInput={(event: Event) =>\n this.dsChange.emit({\n filterId: filter.id,\n value: (event.target as HTMLInputElement).value,\n })\n }\n />\n {dateValue ? (\n <ds-button-unfilled\n variant=\"icon\"\n icon=\"CrossCircle\"\n size=\"sm\"\n isInset\n hasBorder={false}\n rounded\n ariaLabel={`${this.clearLabel} ${filter.fieldLabel ?? filter.label}`}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsChange.emit({ filterId: filter.id, value: '' });\n }}\n />\n ) : null}\n </div>\n </div>\n );\n }\n\n private get captionIconOnly(): boolean {\n return (\n this.collapseLabel &&\n this.captionCompact &&\n this.usesInternalTrigger &&\n Boolean(this.icon)\n );\n }\n\n private syncCaptionCompactObserver(): void {\n this.disconnectCaptionCompactObserver();\n if (!this.collapseLabel) {\n if (this.captionCompact) this.captionCompact = false;\n return;\n }\n this.captionCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.captionCompact !== compact) this.captionCompact = compact;\n });\n }\n\n private disconnectCaptionCompactObserver(): void {\n this.captionCompactDisconnect?.();\n this.captionCompactDisconnect = undefined;\n }\n\n private renderCaptionTooltip(trigger: VNode, label: string): VNode {\n if (!this.collapseLabel) return trigger;\n return (\n <ds-tooltip label={label} side=\"top\" size=\"sm\">\n {trigger}\n </ds-tooltip>\n );\n }\n\n render() {\n const state =\n this.closing && this.closingSnapshot\n ? this.closingSnapshot\n : {\n filters: this.filters,\n values: this.values,\n activeFilterId: this.activeFilterId,\n };\n const activeFilter = this.selectedFilter(state.filters, state.activeFilterId);\n const totalSelected = this.totalSelected(state.filters, state.values);\n const activeFilterCount = this.activeFilterCount(state.filters, state.values);\n const hasActiveFilters = activeFilterCount > 0;\n const label = `${this.triggerLabel}${hasActiveFilters ? ` · ${activeFilterCount}` : ''}`;\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n const popupId = `${this.generatedId}-popup`;\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n width: this.menuWidth,\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n return (\n <Host\n class={{\n 'filter-menu-host': true,\n 'filter-menu-host--external-trigger': !this.usesInternalTrigger,\n 'ds-select-trigger-host': this.usesInternalTrigger,\n [`ds-control--${this.size}`]: true,\n 'ds-table-caption-control': this.collapseLabel,\n 'ds-table-caption-control--compact': this.captionIconOnly,\n ...controlWidthClass(this.width),\n }}\n >\n {this.usesInternalTrigger ? (\n this.renderCaptionTooltip(\n <button\n ref={element => {\n this.triggerElement = (element as HTMLButtonElement) ?? null;\n }}\n id={this.inputId ?? `${this.generatedId}-trigger`}\n type=\"button\"\n class={{\n trigger: true,\n 'ds-control-frame': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': this.activeFill && hasActiveFilters,\n 'trigger--expanded': this.open || this.closing,\n 'trigger--bordered': this.hasBorder,\n 'trigger--has-value': hasActiveFilters,\n [`ds-control--${this.size}`]: true,\n }}\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n aria-expanded={String(this.open)}\n aria-controls={this.open ? popupId : undefined}\n aria-label={this.ariaLabel ?? this.menuLabel}\n onClick={(event: MouseEvent) => this.toggleFromTrigger(event)}\n onKeyDown={(event: KeyboardEvent) => this.handleTriggerKeyDown(event)}\n >\n {this.icon ? (\n <span\n class=\"trigger__prefix ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n </span>\n ) : null}\n {this.captionIconOnly ? null : (\n <ds-text\n class=\"trigger__label-box trigger__label trigger__label-content ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant={textVariant}\n color=\"inherit\"\n lineTruncation={1}\n >\n {label}\n </ds-text>\n )}\n {this.captionIconOnly ? null : (\n <span\n class=\"trigger__chevron ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name=\"ChevronDown\" size={iconSize} color=\"inherit\" />\n </span>\n )}\n </button>,\n this.captionIconOnly ? label : ''\n )\n ) : null}\n\n {this.shouldRender ? (\n <div\n id={popupId}\n popover=\"manual\"\n class={{\n 'filter-menu-popup': true,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={popupStyle}\n role=\"dialog\"\n aria-label={this.menuLabel}\n >\n {activeFilter ? (\n <div class=\"filter-menu\">\n <div class=\"filter-menu__body\">\n <div\n class=\"filter-menu__categories ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n role=\"tablist\"\n aria-label={this.categoriesLabel}\n aria-orientation=\"vertical\"\n >\n {state.filters.map(filter => {\n const active = filter.id === activeFilter.id;\n const count = this.selectedCount(filter, state.values);\n return (\n <div\n key={filter.id}\n class={{\n 'filter-menu__category-section': true,\n 'ds-choice-section--divided': Boolean(filter.divider),\n }}\n >\n <button\n id={`${this.generatedId}-${filter.id}-tab`}\n type=\"button\"\n role=\"tab\"\n data-filter-category={filter.id}\n class={{\n 'filter-menu__category': true,\n 'filter-menu__category--active': active,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': active && this.focusRingVisible,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': active,\n }}\n aria-selected={String(active)}\n aria-controls={`${this.generatedId}-panel`}\n tabIndex={active ? 0 : -1}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.selectCategory(filter.id)}\n onKeyDown={(event: KeyboardEvent) =>\n this.handleCategoryKeyDown(event, state.filters, filter.id)\n }\n >\n <ds-text\n class=\"ds-choice-item__content ds-choice-item__label ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={active ? 'primary' : 'secondary'}\n >\n {filter.label}\n </ds-text>\n {count > 0 ? (\n <ds-tag\n class=\"ds-choice-item__tag ds-interaction-fill__content\"\n label={String(count)}\n size=\"sm\"\n intent=\"brand\"\n contrast=\"bold\"\n isInset\n rounded\n />\n ) : null}\n </button>\n </div>\n );\n })}\n </div>\n\n <div\n id={`${this.generatedId}-panel`}\n class=\"filter-menu__options\"\n role=\"tabpanel\"\n aria-labelledby={`${this.generatedId}-${activeFilter.id}-tab`}\n >\n <div\n key={activeFilter.id}\n class=\"filter-menu__option-list ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n role=\"listbox\"\n aria-label={activeFilter.label}\n aria-multiselectable={activeFilter.kind === 'multiple' ? 'true' : undefined}\n >\n {this.renderOptions(activeFilter, state.values)}\n </div>\n </div>\n </div>\n\n {totalSelected > 0 ? (\n <div class=\"filter-menu__footer ds-choice-footer\">\n <div class=\"ds-choice-footer__content ds-control--md\">\n <ds-text\n class=\"ds-choice-footer__summary\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-live=\"polite\"\n >\n {totalSelected} selected\n </ds-text>\n <button\n class=\"filter-menu__clear ds-choice-footer__clear ds-text-action\"\n type=\"button\"\n onClick={() => this.dsClear.emit()}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {this.clearLabel}\n </ds-text>\n </button>\n </div>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n ) : null}\n </Host>\n );\n }\n}\n",
665
680
  "type": "registry:ui"
666
681
  }
667
682
  ]
@@ -51,6 +51,7 @@
51
51
  "Choose the native input type that matches the value so browsers and assistive technology can provide appropriate semantics and keyboards.",
52
52
  "Error text is associated with the native input and announced as an alert when the invalid state is rendered.",
53
53
  "Read-only fields remain focusable and submittable while preventing edits; inactive or disabled fields leave the tab order and are omitted from form submission.",
54
+ "Number stepper actions are pointer affordances hidden from the accessibility tree; the native spinbutton remains the single keyboard and assistive-technology control.",
54
55
  "Suffix content must not carry information that is otherwise absent from the field's visible label, value, or description."
55
56
  ],
56
57
  "states": [
@@ -61,7 +62,8 @@
61
62
  "Search fields show a standard borderless CrossCircle icon action only while they contain a value. Clearing emits the empty value and restores focus to the native field.",
62
63
  "The suffix slot is for compact units or decorations. The optional leading icon is decorative and does not replace the field label.",
63
64
  "Use autoComplete, inputMode, and enterKeyHint to provide browser and virtual-keyboard hints without changing the value's semantic type.",
64
- "Number fields preserve native spinbutton semantics, stepping, and min/max constraint behavior while dsChange continues to report the native string value.",
65
+ "Number fields preserve native spinbutton semantics, keyboard stepping, and min/max constraint behavior while dsChange continues to report the native string value. Two borderless inset chevron actions split the available inner height, disable independently at their numeric bound, and restore focus to the spinbutton after pointer activation.",
66
+ "textAlign positions the editable value while the numeric stepper stays on the opposite inline edge, keeping the actions clear of the number: start-aligned values use an inline-end stepper and end-aligned values use an inline-start stepper.",
65
67
  "The host exposes focused, filled, dirty, touched, invalid, disabled, read-only, and required data attributes for Field integration and diagnostics.",
66
68
  "Use Input only on primary or secondary application backgrounds. It intentionally does not expose alternate surface contexts."
67
69
  ],
@@ -297,6 +299,15 @@
297
299
  "mutable": false,
298
300
  "description": "Numeric increment used by native stepping and constraint validation."
299
301
  },
302
+ "textAlign": {
303
+ "type": "InputTextAlign",
304
+ "resolvedType": "\"end\" | \"start\"",
305
+ "attribute": "text-align",
306
+ "default": "'start'",
307
+ "required": false,
308
+ "mutable": false,
309
+ "description": "Align the editable value; number steppers follow the same inline edge."
310
+ },
300
311
  "type": {
301
312
  "type": "InputType",
302
313
  "resolvedType": "\"email\" | \"number\" | \"password\" | \"search\" | \"tel\" | \"text\" | \"url\"",
@@ -547,6 +558,15 @@
547
558
  "mutable": false,
548
559
  "description": "Numeric increment used by native stepping and constraint validation."
549
560
  },
561
+ "textAlign": {
562
+ "type": "InputTextAlign",
563
+ "resolvedType": "\"end\" | \"start\"",
564
+ "attribute": "text-align",
565
+ "default": "'start'",
566
+ "required": false,
567
+ "mutable": false,
568
+ "description": "Align the editable value; number steppers follow the same inline edge."
569
+ },
550
570
  "type": {
551
571
  "type": "InputType",
552
572
  "resolvedType": "\"email\" | \"number\" | \"password\" | \"search\" | \"tel\" | \"text\" | \"url\"",
@@ -643,12 +663,12 @@
643
663
  "files": [
644
664
  {
645
665
  "path": "src/wc/components/Input/Input.css",
646
- "content": "@import '../../utils/interaction-fill.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/control-density.css';\n@import '../../utils/control-parts.css';\n@import '../../utils/control-width.css';\n@import '../../utils/field-stack.css';\n@import '../../utils/typography.css';\n@import '../../utils/forced-colors.css';\n\n:host(.input-host) {\n display: inline-flex;\n --ds-input-fg: var(--color-foreground-primary);\n --ds-input-placeholder: var(--color-foreground-secondary);\n --ds-input-placeholder-active: var(--color-foreground-quaternary);\n --ds-input-adornment-fg: var(--color-foreground-secondary);\n --ds-input-border: var(--color-border-secondary);\n}\n\n:host(.ds-control-width--fill),\n:host(.ds-control-width--fill) .input-control {\n width: 100%;\n}\n\n.input-control {\n overflow: hidden;\n background: transparent;\n border: none;\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n color: var(--ds-input-fg);\n}\n\n.input-control--bordered {\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--ds-input-border);\n}\n\n.input-control--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.input-control--bordered:focus-within:not(.input-control--error) {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-brand);\n}\n\n:host(.ds-control-inactive) .input-control::after {\n background: transparent;\n}\n\n.native-input {\n position: relative;\n z-index: 2;\n flex: 1 1 auto;\n min-width: 0;\n height: 100%;\n background: transparent;\n border: none;\n border-radius: inherit;\n outline: none;\n color: var(--ds-input-fg);\n font-family: inherit;\n text-overflow: ellipsis;\n -webkit-appearance: none;\n appearance: none;\n}\n\n.native-input:disabled {\n cursor: not-allowed;\n}\n\n.native-input:read-only:not(:disabled) {\n cursor: default;\n}\n\n.native-input::placeholder {\n color: var(--ds-input-placeholder);\n}\n\n.native-input:focus::placeholder {\n color: var(--ds-input-placeholder-active);\n}\n\n.native-input[type='search']::-webkit-search-cancel-button,\n.native-input[type='search']::-webkit-search-decoration {\n display: none;\n}\n\n.input-control__prefix,\n.input-control__suffix {\n position: relative;\n z-index: 2;\n color: var(--ds-input-adornment-fg);\n}\n\n.input-control__suffix--empty {\n display: none;\n}\n\n/* Keep this more specific than ButtonUnfilled's host-level hug alignment so\n component stylesheet hydration order cannot move the nested action. */\n.input-control > .input-control__clear {\n position: relative;\n z-index: 2;\n flex-shrink: 0;\n align-self: center;\n}\n\n.input-control.ds-control--lg:has(.input-control__clear),\n.input-control.ds-control--md:has(.input-control__clear),\n.input-control.ds-control--sm:has(.input-control__clear) {\n padding-right: var(--dimension-space-050);\n}\n\n.input-control.ds-control--xs:has(.input-control__clear) {\n padding-right: 0;\n}\n\n.error-text {\n width: 100%;\n}\n\n@media (forced-colors: active) {\n .input-control--bordered:focus-within:not(.input-control--error)::after {\n outline-color: var(--ds-forced-color-selected);\n }\n\n .input-control--error::after {\n outline-style: dashed;\n }\n}\n",
666
+ "content": "@import '../../utils/interaction-fill.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/control-density.css';\n@import '../../utils/control-parts.css';\n@import '../../utils/control-width.css';\n@import '../../utils/field-stack.css';\n@import '../../utils/typography.css';\n@import '../../utils/forced-colors.css';\n\n:host(.input-host) {\n display: inline-flex;\n --ds-input-fg: var(--color-foreground-primary);\n --ds-input-placeholder: var(--color-foreground-secondary);\n --ds-input-placeholder-active: var(--color-foreground-quaternary);\n --ds-input-adornment-fg: var(--color-foreground-secondary);\n --ds-input-border: var(--color-border-secondary);\n --ds-input-stepper-fg: var(--color-foreground-secondary);\n}\n\n:host(.ds-control-width--fill),\n:host(.ds-control-width--fill) .input-control {\n width: 100%;\n}\n\n.input-control {\n overflow: hidden;\n background: transparent;\n border: none;\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n color: var(--ds-input-fg);\n}\n\n.input-control--bordered {\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--ds-input-border);\n}\n\n.input-control--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.input-control--bordered:focus-within:not(.input-control--error) {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-brand);\n}\n\n:host(.ds-control-inactive) .input-control::after {\n background: transparent;\n}\n\n.native-input {\n position: relative;\n z-index: 2;\n flex: 1 1 auto;\n min-width: 0;\n height: 100%;\n background: transparent;\n border: none;\n border-radius: inherit;\n outline: none;\n color: var(--ds-input-fg);\n font-family: inherit;\n text-overflow: ellipsis;\n -webkit-appearance: none;\n appearance: none;\n}\n\n.native-input--align-start {\n text-align: start;\n}\n\n.native-input--align-end {\n text-align: end;\n}\n\n/* Replace engine-specific number arrows with one density-aware inset stepper. */\n.native-input[type='number'] {\n -moz-appearance: textfield;\n}\n\n.native-input[type='number']::-webkit-inner-spin-button,\n.native-input[type='number']::-webkit-outer-spin-button {\n margin: 0;\n -webkit-appearance: none;\n}\n\n.input-control--number.input-control--align-start {\n padding-inline-end: var(--dimension-space-025);\n}\n\n.input-control--number.input-control--align-end {\n padding-inline-start: var(--dimension-space-025);\n}\n\n.input-control__number-stepper {\n position: relative;\n z-index: 2;\n display: grid;\n grid-template-rows: repeat(2, minmax(0, 1fr));\n flex: 0 0 var(--ds-control-icon);\n width: var(--ds-control-icon);\n height: calc(var(--ds-control-height) - var(--dimension-space-050));\n overflow: hidden;\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n color: var(--ds-input-stepper-fg);\n}\n\n.input-control__number-step {\n all: unset;\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n color: inherit;\n cursor: default;\n}\n\n.input-control__number-step--increment {\n border-radius: var(--ds-control-radius, var(--dimension-radius-025))\n var(--ds-control-radius, var(--dimension-radius-025)) 0 0;\n}\n\n.input-control__number-step--decrement {\n border-radius: 0 0 var(--ds-control-radius, var(--dimension-radius-025))\n var(--ds-control-radius, var(--dimension-radius-025));\n}\n\n.input-control__number-step:disabled {\n color: var(--color-foreground-quaternary);\n}\n\n.input-control__number-step-icon {\n flex-shrink: 0;\n}\n\n.native-input:disabled {\n cursor: not-allowed;\n}\n\n.native-input:read-only:not(:disabled) {\n cursor: default;\n}\n\n.native-input::placeholder {\n color: var(--ds-input-placeholder);\n}\n\n.native-input:focus::placeholder {\n color: var(--ds-input-placeholder-active);\n}\n\n.native-input[type='search']::-webkit-search-cancel-button,\n.native-input[type='search']::-webkit-search-decoration {\n display: none;\n}\n\n.input-control__prefix,\n.input-control__suffix {\n position: relative;\n z-index: 2;\n color: var(--ds-input-adornment-fg);\n}\n\n.input-control__suffix--empty {\n display: none;\n}\n\n/* Keep this more specific than ButtonUnfilled's host-level hug alignment so\n component stylesheet hydration order cannot move the nested action. */\n.input-control > .input-control__clear {\n position: relative;\n z-index: 2;\n flex-shrink: 0;\n align-self: center;\n}\n\n.input-control.ds-control--lg:has(.input-control__clear),\n.input-control.ds-control--md:has(.input-control__clear),\n.input-control.ds-control--sm:has(.input-control__clear) {\n padding-right: var(--dimension-space-050);\n}\n\n.input-control.ds-control--xs:has(.input-control__clear) {\n padding-right: 0;\n}\n\n.error-text {\n width: 100%;\n}\n\n@media (forced-colors: active) {\n .input-control__number-step:disabled {\n color: var(--ds-forced-color-disabled);\n }\n\n .input-control--bordered:focus-within:not(.input-control--error)::after {\n outline-color: var(--ds-forced-color-selected);\n }\n\n .input-control--error::after {\n outline-style: dashed;\n }\n}\n",
647
667
  "type": "registry:ui"
648
668
  },
649
669
  {
650
670
  "path": "src/wc/components/Input/Input.tsx",
651
- "content": "import { AttachInternals, Component, Prop, State, Event, EventEmitter, Element, Method, Watch, h, Host } from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringFormState,\n setFormControlValue,\n setRequiredValidity,\n type ControlWidth,\n} from '../../utils';\n\nexport type InputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'password' | 'number';\nexport type InputSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type InputWidth = ControlWidth;\n\nconst ICON_SIZE: Record<InputSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst CLEAR_BUTTON_SIZE: Record<InputSize, 'md' | 'sm' | 'xs'> = {\n lg: 'md',\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-input',\n styleUrl: 'Input.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Input {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private generatedId = `ds-input-${++idCounter}`;\n private errorId = `${this.generatedId}-error`;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keeps the value focusable and submittable while preventing edits. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() clearLabel: string = 'Clear';\n @Prop() placeholder: string | undefined;\n @Prop() type: InputType = 'text';\n /** Minimum accepted value when type is number. */\n @Prop() min: number | undefined;\n /** Maximum accepted value when type is number. */\n @Prop() max: number | undefined;\n /** Numeric increment used by native stepping and constraint validation. */\n @Prop() step: number | undefined;\n /** Native browser autofill hint. */\n @Prop({ attribute: 'autocomplete' }) autoComplete: string | undefined;\n /** Preferred virtual keyboard without changing the value semantics. */\n @Prop({ attribute: 'inputmode' }) inputMode: string = '';\n /** Preferred virtual-keyboard action label. */\n @Prop({ attribute: 'enterkeyhint' }) enterKeyHint: string = '';\n /** Control density. */\n @Prop() size: InputSize = 'md';\n /** Width fit — fill the parent (default) or hug the available content. */\n @Prop() width: InputWidth = 'fill';\n /** Show the standard inset border, including focused and invalid strokes. */\n @Prop() hasBorder: boolean = true;\n /** Optional leading icon name. */\n @Prop() icon: string | undefined;\n @Prop() isInactive: boolean = false;\n @Prop() autoFocus: boolean = false;\n @Prop() error: boolean = false;\n @Prop() errorMessage: string | undefined;\n /** Associates the internal input with an external <label>. */\n @Prop() inputId: string | undefined;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n @Event() dsChange!: EventEmitter<string>;\n @Event() dsClear!: EventEmitter<void>;\n\n private initialValue = '';\n @State() private formDisabled = false;\n @State() private hasSuffix = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n this.syncFormValue();\n }\n\n componentDidRender() {\n const hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n if (hasSuffix !== this.hasSuffix) this.hasSuffix = hasSuffix;\n }\n\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n setFormControlValue(this.internals, this.value, { inactive });\n const missing = this.required && !inactive && this.value.length === 0;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n }\n\n @Method()\n async setFocus() {\n this.el.querySelector('input')?.focus();\n }\n\n private handleInput = (e: Event) => {\n this.value = (e.target as HTMLInputElement).value;\n this.dsChange.emit(this.value);\n };\n\n private handleFocus = () => {\n this.focused = true;\n };\n\n private handleBlur = () => {\n this.focused = false;\n this.touched = true;\n };\n\n private handleClear = () => {\n this.value = '';\n this.dsChange.emit('');\n this.dsClear.emit();\n this.el.querySelector('input')?.focus();\n };\n\n render() {\n const inputId = this.inputId ?? this.generatedId;\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const filled = this.value.length > 0;\n const dirty = this.value !== this.initialValue;\n const showClear = this.type === 'search' && filled && !inactive && !this.readOnly;\n const showError = this.error && Boolean(this.errorMessage);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n const numeric = this.type === 'number';\n\n const describedBy = [\n this.ariaDescribedby,\n showError ? this.errorId : undefined,\n ].filter(Boolean).join(' ') || undefined;\n\n return (\n <Host\n class={{\n 'input-host': true,\n 'ds-field-stack': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n }}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-invalid={this.error ? '' : undefined}\n data-filled={filled ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n >\n <div\n class={{\n 'input-control': true,\n 'ds-control-frame': true,\n 'input-control--bordered': this.hasBorder,\n 'input-control--error': this.hasBorder && this.error,\n 'ds-interaction-fill': true,\n [`ds-control--${this.size}`]: true,\n }}\n >\n {this.icon && (\n <span class=\"input-control__prefix ds-control-icon-box ds-interaction-fill__content\" aria-hidden=\"true\">\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n </span>\n )}\n <input\n type={this.type}\n id={inputId}\n value={this.value}\n placeholder={this.placeholder}\n min={numeric ? this.min : undefined}\n max={numeric ? this.max : undefined}\n step={numeric ? this.step : undefined}\n disabled={inactive}\n readOnly={this.readOnly}\n required={this.required}\n autoFocus={this.autoFocus}\n autoComplete={this.autoComplete}\n inputMode={this.inputMode || undefined}\n enterKeyHint={this.enterKeyHint || undefined}\n class={`native-input ds-control-label-box ds-text--${textVariant.replace('text-', '')} ds-text--regular ds-interaction-fill__content`}\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.error ? 'true' : undefined}\n onInput={this.handleInput}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n />\n <span class={{ 'input-control__suffix': true, 'ds-control-icon-box': true, 'input-control__suffix--empty': !this.hasSuffix }}>\n <slot name=\"suffix\" />\n </span>\n {showClear && (\n <ds-button-unfilled\n class=\"input-control__clear\"\n variant=\"icon\"\n size={CLEAR_BUTTON_SIZE[this.size]}\n icon=\"CrossCircle\"\n hasBorder={false}\n rounded\n ariaLabel={this.clearLabel}\n onDsClick={this.handleClear}\n />\n )}\n </div>\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
671
+ "content": "import { AttachInternals, Component, Prop, State, Event, EventEmitter, Element, Method, Watch, h, Host } from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringFormState,\n setFormControlValue,\n setRequiredValidity,\n type ControlWidth,\n} from '../../utils';\n\nexport type InputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'password' | 'number';\nexport type InputSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type InputWidth = ControlWidth;\nexport type InputTextAlign = 'start' | 'end';\n\nconst ICON_SIZE: Record<InputSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst CLEAR_BUTTON_SIZE: Record<InputSize, 'md' | 'sm' | 'xs'> = {\n lg: 'md',\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-input',\n styleUrl: 'Input.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Input {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private generatedId = `ds-input-${++idCounter}`;\n private errorId = `${this.generatedId}-error`;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keeps the value focusable and submittable while preventing edits. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() clearLabel: string = 'Clear';\n @Prop() placeholder: string | undefined;\n @Prop() type: InputType = 'text';\n /** Minimum accepted value when type is number. */\n @Prop() min: number | undefined;\n /** Maximum accepted value when type is number. */\n @Prop() max: number | undefined;\n /** Numeric increment used by native stepping and constraint validation. */\n @Prop() step: number | undefined;\n /** Align the editable value; number steppers follow the same inline edge. */\n @Prop() textAlign: InputTextAlign = 'start';\n /** Native browser autofill hint. */\n @Prop({ attribute: 'autocomplete' }) autoComplete: string | undefined;\n /** Preferred virtual keyboard without changing the value semantics. */\n @Prop({ attribute: 'inputmode' }) inputMode: string = '';\n /** Preferred virtual-keyboard action label. */\n @Prop({ attribute: 'enterkeyhint' }) enterKeyHint: string = '';\n /** Control density. */\n @Prop() size: InputSize = 'md';\n /** Width fit — fill the parent (default) or hug the available content. */\n @Prop() width: InputWidth = 'fill';\n /** Show the standard inset border, including focused and invalid strokes. */\n @Prop() hasBorder: boolean = true;\n /** Optional leading icon name. */\n @Prop() icon: string | undefined;\n @Prop() isInactive: boolean = false;\n @Prop() autoFocus: boolean = false;\n @Prop() error: boolean = false;\n @Prop() errorMessage: string | undefined;\n /** Associates the internal input with an external <label>. */\n @Prop() inputId: string | undefined;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n @Event() dsChange!: EventEmitter<string>;\n @Event() dsClear!: EventEmitter<void>;\n\n private initialValue = '';\n private inputEl?: HTMLInputElement;\n @State() private formDisabled = false;\n @State() private hasSuffix = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n this.syncFormValue();\n }\n\n componentDidRender() {\n const hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n if (hasSuffix !== this.hasSuffix) this.hasSuffix = hasSuffix;\n }\n\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n setFormControlValue(this.internals, this.value, { inactive });\n const missing = this.required && !inactive && this.value.length === 0;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n }\n\n @Method()\n async setFocus() {\n this.inputEl?.focus();\n }\n\n private handleInput = (e: Event) => {\n this.value = (e.target as HTMLInputElement).value;\n this.dsChange.emit(this.value);\n };\n\n private handleFocus = () => {\n this.focused = true;\n };\n\n private handleBlur = () => {\n this.focused = false;\n this.touched = true;\n };\n\n private handleClear = () => {\n this.value = '';\n this.dsChange.emit('');\n this.dsClear.emit();\n this.inputEl?.focus();\n };\n\n private numericStepDisabled(direction: 1 | -1): boolean {\n if (this.value.trim() === '') return false;\n const value = Number(this.value);\n if (!Number.isFinite(value)) return false;\n if (direction > 0 && this.max !== undefined) return value >= this.max;\n if (direction < 0 && this.min !== undefined) return value <= this.min;\n return false;\n }\n\n private handleNumericStep = (direction: 1 | -1) => {\n const input = this.inputEl;\n if (!input || input.disabled || input.readOnly || this.numericStepDisabled(direction)) return;\n\n const previous = input.value;\n if (direction > 0) input.stepUp();\n else input.stepDown();\n input.focus({ preventScroll: true });\n if (input.value === previous) return;\n\n this.value = input.value;\n this.dsChange.emit(this.value);\n };\n\n private renderNumericStepper(inactive: boolean) {\n const incrementDisabled = inactive || this.readOnly || this.numericStepDisabled(1);\n const decrementDisabled = inactive || this.readOnly || this.numericStepDisabled(-1);\n\n return (\n <span class=\"input-control__number-stepper\">\n <button\n type=\"button\"\n class=\"input-control__number-step input-control__number-step--increment ds-focus-ring-inset ds-interaction-fill\"\n disabled={incrementDisabled}\n tabIndex={-1}\n aria-hidden=\"true\"\n onClick={() => this.handleNumericStep(1)}\n >\n <ds-icon\n class=\"input-control__number-step-icon ds-interaction-fill__content\"\n name=\"ChevronUp\"\n size=\"xs\"\n color=\"inherit\"\n />\n </button>\n <button\n type=\"button\"\n class=\"input-control__number-step input-control__number-step--decrement ds-focus-ring-inset ds-interaction-fill\"\n disabled={decrementDisabled}\n tabIndex={-1}\n aria-hidden=\"true\"\n onClick={() => this.handleNumericStep(-1)}\n >\n <ds-icon\n class=\"input-control__number-step-icon ds-interaction-fill__content\"\n name=\"ChevronDown\"\n size=\"xs\"\n color=\"inherit\"\n />\n </button>\n </span>\n );\n }\n\n render() {\n const inputId = this.inputId ?? this.generatedId;\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const filled = this.value.length > 0;\n const dirty = this.value !== this.initialValue;\n const showClear = this.type === 'search' && filled && !inactive && !this.readOnly;\n const showError = this.error && Boolean(this.errorMessage);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n const numeric = this.type === 'number';\n\n const describedBy = [\n this.ariaDescribedby,\n showError ? this.errorId : undefined,\n ].filter(Boolean).join(' ') || undefined;\n\n return (\n <Host\n class={{\n 'input-host': true,\n 'ds-field-stack': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n }}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-invalid={this.error ? '' : undefined}\n data-filled={filled ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n >\n <div\n class={{\n 'input-control': true,\n 'input-control--number': numeric,\n 'input-control--align-start': this.textAlign === 'start',\n 'input-control--align-end': this.textAlign === 'end',\n 'ds-control-frame': true,\n 'input-control--bordered': this.hasBorder,\n 'input-control--error': this.hasBorder && this.error,\n 'ds-interaction-fill': true,\n [`ds-control--${this.size}`]: true,\n }}\n >\n {numeric && this.textAlign === 'end' && this.renderNumericStepper(inactive)}\n {this.icon && (\n <span class=\"input-control__prefix ds-control-icon-box ds-interaction-fill__content\" aria-hidden=\"true\">\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n </span>\n )}\n <input\n ref={element => {\n this.inputEl = element;\n }}\n type={this.type}\n id={inputId}\n value={this.value}\n placeholder={this.placeholder}\n min={numeric ? this.min : undefined}\n max={numeric ? this.max : undefined}\n step={numeric ? this.step : undefined}\n disabled={inactive}\n readOnly={this.readOnly}\n required={this.required}\n autoFocus={this.autoFocus}\n autoComplete={this.autoComplete}\n inputMode={this.inputMode || undefined}\n enterKeyHint={this.enterKeyHint || undefined}\n class={`native-input native-input--align-${this.textAlign} ds-control-label-box ds-text--${textVariant.replace('text-', '')} ds-text--regular ds-interaction-fill__content`}\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.error ? 'true' : undefined}\n onInput={this.handleInput}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n />\n <span class={{ 'input-control__suffix': true, 'ds-control-icon-box': true, 'input-control__suffix--empty': !this.hasSuffix }}>\n <slot name=\"suffix\" />\n </span>\n {showClear && (\n <ds-button-unfilled\n class=\"input-control__clear\"\n variant=\"icon\"\n size={CLEAR_BUTTON_SIZE[this.size]}\n icon=\"CrossCircle\"\n hasBorder={false}\n rounded\n ariaLabel={this.clearLabel}\n onDsClick={this.handleClear}\n />\n )}\n {numeric && this.textAlign === 'start' && this.renderNumericStepper(inactive)}\n </div>\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
652
672
  "type": "registry:ui"
653
673
  }
654
674
  ]