@lo-tek/interfaces 0.0.0-stage → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/CHANGELOG.md +3254 -0
  2. package/LICENSE +21 -0
  3. package/README.md +129 -2
  4. package/agent/astro.md +400 -0
  5. package/agent/bundles.md +429 -0
  6. package/agent/datastar.md +409 -0
  7. package/agent/html.md +388 -0
  8. package/agent/react.md +440 -0
  9. package/agent/templates.md +472 -0
  10. package/core/lotek-core.wasm +0 -0
  11. package/core/lotek.wasm +0 -0
  12. package/dist/cli.d.ts +12 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +14 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/components/css/accordion/accordion.d.ts +37 -0
  17. package/dist/components/css/accordion/accordion.d.ts.map +1 -0
  18. package/dist/components/css/accordion/accordion.js +31 -0
  19. package/dist/components/css/accordion/accordion.js.map +1 -0
  20. package/dist/components/css/alert/alert.d.ts +35 -0
  21. package/dist/components/css/alert/alert.d.ts.map +1 -0
  22. package/dist/components/css/alert/alert.js +29 -0
  23. package/dist/components/css/alert/alert.js.map +1 -0
  24. package/dist/components/css/avatar/avatar.d.ts +40 -0
  25. package/dist/components/css/avatar/avatar.d.ts.map +1 -0
  26. package/dist/components/css/avatar/avatar.js +30 -0
  27. package/dist/components/css/avatar/avatar.js.map +1 -0
  28. package/dist/components/css/badge/badge.d.ts +24 -0
  29. package/dist/components/css/badge/badge.d.ts.map +1 -0
  30. package/dist/components/css/badge/badge.js +24 -0
  31. package/dist/components/css/badge/badge.js.map +1 -0
  32. package/dist/components/css/breadcrumbs/breadcrumbs.d.ts +26 -0
  33. package/dist/components/css/breadcrumbs/breadcrumbs.d.ts.map +1 -0
  34. package/dist/components/css/breadcrumbs/breadcrumbs.js +24 -0
  35. package/dist/components/css/breadcrumbs/breadcrumbs.js.map +1 -0
  36. package/dist/components/css/button/button.d.ts +26 -0
  37. package/dist/components/css/button/button.d.ts.map +1 -0
  38. package/dist/components/css/button/button.js +25 -0
  39. package/dist/components/css/button/button.js.map +1 -0
  40. package/dist/components/css/card/card.d.ts +36 -0
  41. package/dist/components/css/card/card.d.ts.map +1 -0
  42. package/dist/components/css/card/card.js +28 -0
  43. package/dist/components/css/card/card.js.map +1 -0
  44. package/dist/components/css/checkbox/checkbox.d.ts +32 -0
  45. package/dist/components/css/checkbox/checkbox.d.ts.map +1 -0
  46. package/dist/components/css/checkbox/checkbox.js +27 -0
  47. package/dist/components/css/checkbox/checkbox.js.map +1 -0
  48. package/dist/components/css/divider/divider.d.ts +29 -0
  49. package/dist/components/css/divider/divider.d.ts.map +1 -0
  50. package/dist/components/css/divider/divider.js +23 -0
  51. package/dist/components/css/divider/divider.js.map +1 -0
  52. package/dist/components/css/error-text/error-text.d.ts +24 -0
  53. package/dist/components/css/error-text/error-text.d.ts.map +1 -0
  54. package/dist/components/css/error-text/error-text.js +18 -0
  55. package/dist/components/css/error-text/error-text.js.map +1 -0
  56. package/dist/components/css/fieldset/fieldset.d.ts +55 -0
  57. package/dist/components/css/fieldset/fieldset.d.ts.map +1 -0
  58. package/dist/components/css/fieldset/fieldset.js +39 -0
  59. package/dist/components/css/fieldset/fieldset.js.map +1 -0
  60. package/dist/components/css/file-upload/file-upload.d.ts +41 -0
  61. package/dist/components/css/file-upload/file-upload.d.ts.map +1 -0
  62. package/dist/components/css/file-upload/file-upload.js +32 -0
  63. package/dist/components/css/file-upload/file-upload.js.map +1 -0
  64. package/dist/components/css/heading/heading.d.ts +29 -0
  65. package/dist/components/css/heading/heading.d.ts.map +1 -0
  66. package/dist/components/css/heading/heading.js +23 -0
  67. package/dist/components/css/heading/heading.js.map +1 -0
  68. package/dist/components/css/help-text/help-text.d.ts +24 -0
  69. package/dist/components/css/help-text/help-text.d.ts.map +1 -0
  70. package/dist/components/css/help-text/help-text.js +23 -0
  71. package/dist/components/css/help-text/help-text.js.map +1 -0
  72. package/dist/components/css/icon/icon.d.ts +34 -0
  73. package/dist/components/css/icon/icon.d.ts.map +1 -0
  74. package/dist/components/css/icon/icon.js +25 -0
  75. package/dist/components/css/icon/icon.js.map +1 -0
  76. package/dist/components/css/input/input.d.ts +40 -0
  77. package/dist/components/css/input/input.d.ts.map +1 -0
  78. package/dist/components/css/input/input.js +45 -0
  79. package/dist/components/css/input/input.js.map +1 -0
  80. package/dist/components/css/label/label.d.ts +33 -0
  81. package/dist/components/css/label/label.d.ts.map +1 -0
  82. package/dist/components/css/label/label.js +23 -0
  83. package/dist/components/css/label/label.js.map +1 -0
  84. package/dist/components/css/link/link.d.ts +28 -0
  85. package/dist/components/css/link/link.d.ts.map +1 -0
  86. package/dist/components/css/link/link.js +20 -0
  87. package/dist/components/css/link/link.js.map +1 -0
  88. package/dist/components/css/list/list.d.ts +31 -0
  89. package/dist/components/css/list/list.d.ts.map +1 -0
  90. package/dist/components/css/list/list.js +25 -0
  91. package/dist/components/css/list/list.js.map +1 -0
  92. package/dist/components/css/pagination/pagination.d.ts +27 -0
  93. package/dist/components/css/pagination/pagination.d.ts.map +1 -0
  94. package/dist/components/css/pagination/pagination.js +25 -0
  95. package/dist/components/css/pagination/pagination.js.map +1 -0
  96. package/dist/components/css/paragraph/paragraph.d.ts +36 -0
  97. package/dist/components/css/paragraph/paragraph.d.ts.map +1 -0
  98. package/dist/components/css/paragraph/paragraph.js +26 -0
  99. package/dist/components/css/paragraph/paragraph.js.map +1 -0
  100. package/dist/components/css/progress/progress.d.ts +53 -0
  101. package/dist/components/css/progress/progress.d.ts.map +1 -0
  102. package/dist/components/css/progress/progress.js +38 -0
  103. package/dist/components/css/progress/progress.js.map +1 -0
  104. package/dist/components/css/radio/radio.d.ts +32 -0
  105. package/dist/components/css/radio/radio.d.ts.map +1 -0
  106. package/dist/components/css/radio/radio.js +27 -0
  107. package/dist/components/css/radio/radio.js.map +1 -0
  108. package/dist/components/css/search/search.d.ts +34 -0
  109. package/dist/components/css/search/search.d.ts.map +1 -0
  110. package/dist/components/css/search/search.js +32 -0
  111. package/dist/components/css/search/search.js.map +1 -0
  112. package/dist/components/css/select/select.d.ts +40 -0
  113. package/dist/components/css/select/select.d.ts.map +1 -0
  114. package/dist/components/css/select/select.js +31 -0
  115. package/dist/components/css/select/select.js.map +1 -0
  116. package/dist/components/css/shared.d.ts +90 -0
  117. package/dist/components/css/shared.d.ts.map +1 -0
  118. package/dist/components/css/shared.js +167 -0
  119. package/dist/components/css/shared.js.map +1 -0
  120. package/dist/components/css/skeleton/skeleton.d.ts +29 -0
  121. package/dist/components/css/skeleton/skeleton.d.ts.map +1 -0
  122. package/dist/components/css/skeleton/skeleton.js +23 -0
  123. package/dist/components/css/skeleton/skeleton.js.map +1 -0
  124. package/dist/components/css/skip-link/skip-link.d.ts +18 -0
  125. package/dist/components/css/skip-link/skip-link.d.ts.map +1 -0
  126. package/dist/components/css/skip-link/skip-link.js +16 -0
  127. package/dist/components/css/skip-link/skip-link.js.map +1 -0
  128. package/dist/components/css/spinner/spinner.d.ts +33 -0
  129. package/dist/components/css/spinner/spinner.d.ts.map +1 -0
  130. package/dist/components/css/spinner/spinner.js +25 -0
  131. package/dist/components/css/spinner/spinner.js.map +1 -0
  132. package/dist/components/css/sr-only/sr-only.d.ts +17 -0
  133. package/dist/components/css/sr-only/sr-only.d.ts.map +1 -0
  134. package/dist/components/css/sr-only/sr-only.js +15 -0
  135. package/dist/components/css/sr-only/sr-only.js.map +1 -0
  136. package/dist/components/css/switch/switch.d.ts +37 -0
  137. package/dist/components/css/switch/switch.d.ts.map +1 -0
  138. package/dist/components/css/switch/switch.js +29 -0
  139. package/dist/components/css/switch/switch.js.map +1 -0
  140. package/dist/components/css/table/table.d.ts +76 -0
  141. package/dist/components/css/table/table.d.ts.map +1 -0
  142. package/dist/components/css/table/table.js +59 -0
  143. package/dist/components/css/table/table.js.map +1 -0
  144. package/dist/components/css/tag/tag.d.ts +35 -0
  145. package/dist/components/css/tag/tag.d.ts.map +1 -0
  146. package/dist/components/css/tag/tag.js +27 -0
  147. package/dist/components/css/tag/tag.js.map +1 -0
  148. package/dist/components/css/textarea/textarea.d.ts +23 -0
  149. package/dist/components/css/textarea/textarea.d.ts.map +1 -0
  150. package/dist/components/css/textarea/textarea.js +18 -0
  151. package/dist/components/css/textarea/textarea.js.map +1 -0
  152. package/dist/components/css/toggle-group/toggle-group.d.ts +31 -0
  153. package/dist/components/css/toggle-group/toggle-group.d.ts.map +1 -0
  154. package/dist/components/css/toggle-group/toggle-group.js +29 -0
  155. package/dist/components/css/toggle-group/toggle-group.js.map +1 -0
  156. package/dist/components/css/tooltip/tooltip.d.ts +31 -0
  157. package/dist/components/css/tooltip/tooltip.d.ts.map +1 -0
  158. package/dist/components/css/tooltip/tooltip.js +29 -0
  159. package/dist/components/css/tooltip/tooltip.js.map +1 -0
  160. package/dist/components/default-texts.d.ts +35 -0
  161. package/dist/components/default-texts.d.ts.map +1 -0
  162. package/dist/components/default-texts.js +35 -0
  163. package/dist/components/default-texts.js.map +1 -0
  164. package/dist/components/events.d.ts +77 -0
  165. package/dist/components/events.d.ts.map +1 -0
  166. package/dist/components/events.js +3 -0
  167. package/dist/components/events.js.map +1 -0
  168. package/dist/components/frame/dialog/dialog.d.ts +122 -0
  169. package/dist/components/frame/dialog/dialog.d.ts.map +1 -0
  170. package/dist/components/frame/dialog/dialog.js +112 -0
  171. package/dist/components/frame/dialog/dialog.js.map +1 -0
  172. package/dist/components/frame/dialog/lt-dialog.d.ts +104 -0
  173. package/dist/components/frame/dialog/lt-dialog.d.ts.map +1 -0
  174. package/dist/components/frame/dialog/lt-dialog.js +431 -0
  175. package/dist/components/frame/dialog/lt-dialog.js.map +1 -0
  176. package/dist/components/frame/error-summary/error-summary.d.ts +40 -0
  177. package/dist/components/frame/error-summary/error-summary.d.ts.map +1 -0
  178. package/dist/components/frame/error-summary/error-summary.js +32 -0
  179. package/dist/components/frame/error-summary/error-summary.js.map +1 -0
  180. package/dist/components/frame/error-summary/lt-error-summary.d.ts +82 -0
  181. package/dist/components/frame/error-summary/lt-error-summary.d.ts.map +1 -0
  182. package/dist/components/frame/error-summary/lt-error-summary.js +302 -0
  183. package/dist/components/frame/error-summary/lt-error-summary.js.map +1 -0
  184. package/dist/components/frame/field/field-core.d.ts +92 -0
  185. package/dist/components/frame/field/field-core.d.ts.map +1 -0
  186. package/dist/components/frame/field/field-core.js +63 -0
  187. package/dist/components/frame/field/field-core.js.map +1 -0
  188. package/dist/components/frame/field/lt-field.d.ts +173 -0
  189. package/dist/components/frame/field/lt-field.d.ts.map +1 -0
  190. package/dist/components/frame/field/lt-field.js +580 -0
  191. package/dist/components/frame/field/lt-field.js.map +1 -0
  192. package/dist/components/frame/popover/lt-popover.d.ts +132 -0
  193. package/dist/components/frame/popover/lt-popover.d.ts.map +1 -0
  194. package/dist/components/frame/popover/lt-popover.js +490 -0
  195. package/dist/components/frame/popover/lt-popover.js.map +1 -0
  196. package/dist/components/frame/popover/popover.d.ts +49 -0
  197. package/dist/components/frame/popover/popover.d.ts.map +1 -0
  198. package/dist/components/frame/popover/popover.js +64 -0
  199. package/dist/components/frame/popover/popover.js.map +1 -0
  200. package/dist/components/frame/session-timeout/lt-session-timeout.d.ts +198 -0
  201. package/dist/components/frame/session-timeout/lt-session-timeout.d.ts.map +1 -0
  202. package/dist/components/frame/session-timeout/lt-session-timeout.js +573 -0
  203. package/dist/components/frame/session-timeout/lt-session-timeout.js.map +1 -0
  204. package/dist/components/frame/session-timeout/session-timeout.d.ts +113 -0
  205. package/dist/components/frame/session-timeout/session-timeout.d.ts.map +1 -0
  206. package/dist/components/frame/session-timeout/session-timeout.js +101 -0
  207. package/dist/components/frame/session-timeout/session-timeout.js.map +1 -0
  208. package/dist/components/frame/suggestion/lt-suggestion.d.ts +361 -0
  209. package/dist/components/frame/suggestion/lt-suggestion.d.ts.map +1 -0
  210. package/dist/components/frame/suggestion/lt-suggestion.js +1152 -0
  211. package/dist/components/frame/suggestion/lt-suggestion.js.map +1 -0
  212. package/dist/components/frame/suggestion/suggestion.d.ts +143 -0
  213. package/dist/components/frame/suggestion/suggestion.d.ts.map +1 -0
  214. package/dist/components/frame/suggestion/suggestion.js +127 -0
  215. package/dist/components/frame/suggestion/suggestion.js.map +1 -0
  216. package/dist/components/frame/tabs/lt-tabs.d.ts +404 -0
  217. package/dist/components/frame/tabs/lt-tabs.d.ts.map +1 -0
  218. package/dist/components/frame/tabs/lt-tabs.js +1201 -0
  219. package/dist/components/frame/tabs/lt-tabs.js.map +1 -0
  220. package/dist/components/frame/tabs/tabs.d.ts +112 -0
  221. package/dist/components/frame/tabs/tabs.d.ts.map +1 -0
  222. package/dist/components/frame/tabs/tabs.js +107 -0
  223. package/dist/components/frame/tabs/tabs.js.map +1 -0
  224. package/dist/components/host-element.d.ts +135 -0
  225. package/dist/components/host-element.d.ts.map +1 -0
  226. package/dist/components/host-element.js +366 -0
  227. package/dist/components/host-element.js.map +1 -0
  228. package/dist/components/standalone/connection-status/connection-status.d.ts +28 -0
  229. package/dist/components/standalone/connection-status/connection-status.d.ts.map +1 -0
  230. package/dist/components/standalone/connection-status/connection-status.js +22 -0
  231. package/dist/components/standalone/connection-status/connection-status.js.map +1 -0
  232. package/dist/components/standalone/connection-status/lt-connection-status.d.ts +81 -0
  233. package/dist/components/standalone/connection-status/lt-connection-status.d.ts.map +1 -0
  234. package/dist/components/standalone/connection-status/lt-connection-status.js +216 -0
  235. package/dist/components/standalone/connection-status/lt-connection-status.js.map +1 -0
  236. package/dist/components/standalone/toast/lt-toast.d.ts +109 -0
  237. package/dist/components/standalone/toast/lt-toast.d.ts.map +1 -0
  238. package/dist/components/standalone/toast/lt-toast.js +256 -0
  239. package/dist/components/standalone/toast/lt-toast.js.map +1 -0
  240. package/dist/components/standalone/toast/toast.d.ts +52 -0
  241. package/dist/components/standalone/toast/toast.d.ts.map +1 -0
  242. package/dist/components/standalone/toast/toast.js +42 -0
  243. package/dist/components/standalone/toast/toast.js.map +1 -0
  244. package/dist/diagnostics/core.d.ts +137 -0
  245. package/dist/diagnostics/core.d.ts.map +1 -0
  246. package/dist/diagnostics/core.js +72 -0
  247. package/dist/diagnostics/core.js.map +1 -0
  248. package/dist/diagnostics/default-core.d.ts +10 -0
  249. package/dist/diagnostics/default-core.d.ts.map +1 -0
  250. package/dist/diagnostics/default-core.js +72 -0
  251. package/dist/diagnostics/default-core.js.map +1 -0
  252. package/dist/diagnostics/index.d.ts +84 -0
  253. package/dist/diagnostics/index.d.ts.map +1 -0
  254. package/dist/diagnostics/index.js +106 -0
  255. package/dist/diagnostics/index.js.map +1 -0
  256. package/dist/dom.d.ts +55 -0
  257. package/dist/dom.d.ts.map +1 -0
  258. package/dist/dom.js +162 -0
  259. package/dist/dom.js.map +1 -0
  260. package/dist/index.d.ts +97 -0
  261. package/dist/index.d.ts.map +1 -0
  262. package/dist/index.js +58 -0
  263. package/dist/index.js.map +1 -0
  264. package/dist/jsx/react.d.ts +180 -0
  265. package/dist/jsx/react.d.ts.map +1 -0
  266. package/dist/jsx/react.js +2 -0
  267. package/dist/jsx/react.js.map +1 -0
  268. package/dist/lotek.css +3617 -0
  269. package/dist/lt.d.ts +325 -0
  270. package/dist/lt.d.ts.map +1 -0
  271. package/dist/lt.js +183 -0
  272. package/dist/lt.js.map +1 -0
  273. package/dist/react.d.ts +492 -0
  274. package/dist/react.d.ts.map +1 -0
  275. package/dist/react.js +296 -0
  276. package/dist/react.js.map +1 -0
  277. package/dist/register.d.ts +17 -0
  278. package/dist/register.d.ts.map +1 -0
  279. package/dist/register.js +35 -0
  280. package/dist/register.js.map +1 -0
  281. package/dist/tokens/color.d.ts +43 -0
  282. package/dist/tokens/color.d.ts.map +1 -0
  283. package/dist/tokens/color.js +123 -0
  284. package/dist/tokens/color.js.map +1 -0
  285. package/dist/tokens/contract.d.ts +18 -0
  286. package/dist/tokens/contract.d.ts.map +1 -0
  287. package/dist/tokens/contract.js +18 -0
  288. package/dist/tokens/contract.js.map +1 -0
  289. package/dist/tokens/matrix-cells.d.ts +30 -0
  290. package/dist/tokens/matrix-cells.d.ts.map +1 -0
  291. package/dist/tokens/matrix-cells.js +273 -0
  292. package/dist/tokens/matrix-cells.js.map +1 -0
  293. package/dist/tokens/matrix.d.ts +18 -0
  294. package/dist/tokens/matrix.d.ts.map +1 -0
  295. package/dist/tokens/matrix.js +21 -0
  296. package/dist/tokens/matrix.js.map +1 -0
  297. package/dist/tokens/theme-check.d.ts +42 -0
  298. package/dist/tokens/theme-check.d.ts.map +1 -0
  299. package/dist/tokens/theme-check.js +10 -0
  300. package/dist/tokens/theme-check.js.map +1 -0
  301. package/dist/tokens/theme.d.ts +94 -0
  302. package/dist/tokens/theme.d.ts.map +1 -0
  303. package/dist/tokens/theme.js +14 -0
  304. package/dist/tokens/theme.js.map +1 -0
  305. package/dist/tokens/tokens.d.ts +122 -0
  306. package/dist/tokens/tokens.d.ts.map +1 -0
  307. package/dist/tokens/tokens.js +158 -0
  308. package/dist/tokens/tokens.js.map +1 -0
  309. package/dist/vocabulary/classes.d.ts +3 -0
  310. package/dist/vocabulary/classes.d.ts.map +1 -0
  311. package/dist/vocabulary/classes.js +1320 -0
  312. package/dist/vocabulary/classes.js.map +1 -0
  313. package/dist/vocabulary/elements.d.ts +3 -0
  314. package/dist/vocabulary/elements.d.ts.map +1 -0
  315. package/dist/vocabulary/elements.js +410 -0
  316. package/dist/vocabulary/elements.js.map +1 -0
  317. package/dist/vocabulary/types.d.ts +175 -0
  318. package/dist/vocabulary/types.d.ts.map +1 -0
  319. package/dist/vocabulary/types.js +11 -0
  320. package/dist/vocabulary/types.js.map +1 -0
  321. package/dist/wasi-host.d.ts +24 -0
  322. package/dist/wasi-host.d.ts.map +1 -0
  323. package/dist/wasi-host.js +140 -0
  324. package/dist/wasi-host.js.map +1 -0
  325. package/live-templates.xml +62 -0
  326. package/manifest/files.json +373 -0
  327. package/manifest/manifest.json +4965 -0
  328. package/manifest/manifest.schema.json +513 -0
  329. package/package.json +404 -4
  330. package/src/cli.ts +21 -0
  331. package/src/components/css/accordion/accordion.css +102 -0
  332. package/src/components/css/accordion/accordion.ts +50 -0
  333. package/src/components/css/alert/alert.css +53 -0
  334. package/src/components/css/alert/alert.ts +48 -0
  335. package/src/components/css/avatar/avatar.css +96 -0
  336. package/src/components/css/avatar/avatar.ts +57 -0
  337. package/src/components/css/badge/badge.css +42 -0
  338. package/src/components/css/badge/badge.ts +43 -0
  339. package/src/components/css/breadcrumbs/breadcrumbs.css +62 -0
  340. package/src/components/css/breadcrumbs/breadcrumbs.ts +30 -0
  341. package/src/components/css/button/button.css +128 -0
  342. package/src/components/css/button/button.ts +44 -0
  343. package/src/components/css/card/card.css +65 -0
  344. package/src/components/css/card/card.ts +50 -0
  345. package/src/components/css/checkbox/checkbox.css +114 -0
  346. package/src/components/css/checkbox/checkbox.ts +50 -0
  347. package/src/components/css/divider/divider.css +38 -0
  348. package/src/components/css/divider/divider.ts +42 -0
  349. package/src/components/css/error-text/error-text.css +21 -0
  350. package/src/components/css/error-text/error-text.ts +37 -0
  351. package/src/components/css/fieldset/fieldset.css +72 -0
  352. package/src/components/css/fieldset/fieldset.ts +87 -0
  353. package/src/components/css/file-upload/file-upload.css +102 -0
  354. package/src/components/css/file-upload/file-upload.ts +65 -0
  355. package/src/components/css/heading/heading.css +33 -0
  356. package/src/components/css/heading/heading.ts +42 -0
  357. package/src/components/css/help-text/help-text.css +24 -0
  358. package/src/components/css/help-text/help-text.ts +42 -0
  359. package/src/components/css/icon/icon.css +20 -0
  360. package/src/components/css/icon/icon.ts +44 -0
  361. package/src/components/css/input/input.css +145 -0
  362. package/src/components/css/input/input.ts +77 -0
  363. package/src/components/css/label/label.css +42 -0
  364. package/src/components/css/label/label.ts +51 -0
  365. package/src/components/css/link/link.css +38 -0
  366. package/src/components/css/link/link.ts +38 -0
  367. package/src/components/css/list/list.css +53 -0
  368. package/src/components/css/list/list.ts +44 -0
  369. package/src/components/css/pagination/pagination.css +95 -0
  370. package/src/components/css/pagination/pagination.ts +33 -0
  371. package/src/components/css/paragraph/paragraph.css +31 -0
  372. package/src/components/css/paragraph/paragraph.ts +53 -0
  373. package/src/components/css/progress/progress.css +161 -0
  374. package/src/components/css/progress/progress.ts +78 -0
  375. package/src/components/css/radio/radio.css +94 -0
  376. package/src/components/css/radio/radio.ts +50 -0
  377. package/src/components/css/search/search.css +50 -0
  378. package/src/components/css/search/search.ts +56 -0
  379. package/src/components/css/select/select.css +321 -0
  380. package/src/components/css/select/select.ts +63 -0
  381. package/src/components/css/shared.ts +199 -0
  382. package/src/components/css/skeleton/skeleton.css +62 -0
  383. package/src/components/css/skeleton/skeleton.ts +43 -0
  384. package/src/components/css/skip-link/skip-link.css +41 -0
  385. package/src/components/css/skip-link/skip-link.ts +22 -0
  386. package/src/components/css/spinner/spinner.css +48 -0
  387. package/src/components/css/spinner/spinner.ts +48 -0
  388. package/src/components/css/sr-only/sr-only.css +16 -0
  389. package/src/components/css/sr-only/sr-only.ts +21 -0
  390. package/src/components/css/switch/switch.css +105 -0
  391. package/src/components/css/switch/switch.ts +50 -0
  392. package/src/components/css/table/table.css +159 -0
  393. package/src/components/css/table/table.ts +107 -0
  394. package/src/components/css/tag/tag.css +58 -0
  395. package/src/components/css/tag/tag.ts +49 -0
  396. package/src/components/css/textarea/textarea.css +83 -0
  397. package/src/components/css/textarea/textarea.ts +40 -0
  398. package/src/components/css/toggle-group/toggle-group.css +92 -0
  399. package/src/components/css/toggle-group/toggle-group.ts +37 -0
  400. package/src/components/css/tooltip/tooltip.css +89 -0
  401. package/src/components/css/tooltip/tooltip.ts +37 -0
  402. package/src/components/default-texts.ts +34 -0
  403. package/src/components/events.ts +68 -0
  404. package/src/components/frame/dialog/dialog.css +306 -0
  405. package/src/components/frame/dialog/dialog.ts +166 -0
  406. package/src/components/frame/dialog/lt-dialog.ts +510 -0
  407. package/src/components/frame/error-summary/error-summary.css +47 -0
  408. package/src/components/frame/error-summary/error-summary.ts +56 -0
  409. package/src/components/frame/error-summary/lt-error-summary.ts +344 -0
  410. package/src/components/frame/field/field-core.ts +152 -0
  411. package/src/components/frame/field/field.css +17 -0
  412. package/src/components/frame/field/lt-field.ts +753 -0
  413. package/src/components/frame/popover/lt-popover.ts +555 -0
  414. package/src/components/frame/popover/popover.css +50 -0
  415. package/src/components/frame/popover/popover.ts +102 -0
  416. package/src/components/frame/session-timeout/lt-session-timeout.ts +681 -0
  417. package/src/components/frame/session-timeout/session-timeout.css +50 -0
  418. package/src/components/frame/session-timeout/session-timeout.ts +156 -0
  419. package/src/components/frame/suggestion/lt-suggestion.ts +1358 -0
  420. package/src/components/frame/suggestion/suggestion.css +132 -0
  421. package/src/components/frame/suggestion/suggestion.ts +172 -0
  422. package/src/components/frame/tabs/lt-tabs.ts +1319 -0
  423. package/src/components/frame/tabs/tabs.css +128 -0
  424. package/src/components/frame/tabs/tabs.ts +188 -0
  425. package/src/components/host-element.ts +400 -0
  426. package/src/components/standalone/connection-status/connection-status.css +41 -0
  427. package/src/components/standalone/connection-status/connection-status.ts +41 -0
  428. package/src/components/standalone/connection-status/lt-connection-status.ts +242 -0
  429. package/src/components/standalone/toast/lt-toast.ts +300 -0
  430. package/src/components/standalone/toast/toast.css +78 -0
  431. package/src/components/standalone/toast/toast.ts +70 -0
  432. package/src/diagnostics/core.ts +210 -0
  433. package/src/diagnostics/default-core.ts +81 -0
  434. package/src/diagnostics/index.ts +150 -0
  435. package/src/dom.ts +185 -0
  436. package/src/index.ts +218 -0
  437. package/src/jsx/react.ts +212 -0
  438. package/src/lt.ts +192 -0
  439. package/src/react.ts +411 -0
  440. package/src/register.ts +43 -0
  441. package/src/tailwind/tailwind.css +169 -0
  442. package/src/tokens/boundary.css +104 -0
  443. package/src/tokens/color-contract.json +231 -0
  444. package/src/tokens/color.ts +150 -0
  445. package/src/tokens/contract.ts +24 -0
  446. package/src/tokens/matrix-cells.ts +375 -0
  447. package/src/tokens/matrix.ts +32 -0
  448. package/src/tokens/theme-check.ts +51 -0
  449. package/src/tokens/theme.ts +104 -0
  450. package/src/tokens/tokens.css +569 -0
  451. package/src/tokens/tokens.ts +223 -0
  452. package/src/vocabulary/classes.ts +1322 -0
  453. package/src/vocabulary/elements.ts +412 -0
  454. package/src/vocabulary/types.ts +178 -0
  455. package/src/wasi-host.ts +155 -0
  456. package/web-types.json +800 -0
@@ -0,0 +1,1152 @@
1
+ import { LABEL_CLASS } from "../../css/label/label.js";
2
+ import { DEFAULT_TEXTS } from "../../default-texts.js";
3
+ import { addClass, defineElement, derivedParts, HostElement, isServerControlled, languageOf, SERVER_CONTROLLED, setAttr, setFlag, setText, uniqueId, warnAboutMarkup, } from "../../host-element.js";
4
+ import { SUGGESTION_EMPTY_CLASS, SUGGESTION_LIST_CLASS, SUGGESTION_OPTION_CLASS, SUGGESTION_VALUE_CLASS, } from "./suggestion.js";
5
+ /**
6
+ * The texts for the number of matches, one attribute per form. `count-none`
7
+ * is zero matches, and the rest are the forms `Intl.PluralRules` knows.
8
+ */
9
+ const COUNT_ATTRIBUTES = [
10
+ "count-none",
11
+ "count-zero",
12
+ "count-one",
13
+ "count-two",
14
+ "count-few",
15
+ "count-many",
16
+ "count-other",
17
+ ];
18
+ export const LT_SUGGESTION_TAG = "lt-suggestion";
19
+ /**
20
+ * The parts the suggestion field finds, as selectors relative to the element.
21
+ * The web component finds its parts with these, and the manifest describes
22
+ * them for whoever writes the markup by hand, so the two cannot disagree.
23
+ */
24
+ export const LT_SUGGESTION_PARTS = {
25
+ /** The field: the role when present, otherwise the first input. */
26
+ control: "[role='combobox'], input:not([type='hidden'])",
27
+ /** The list: the role when present, otherwise the class. */
28
+ list: `[role='listbox'], .${SUGGESTION_LIST_CLASS}`,
29
+ /** An option: the role when present, otherwise an `<li>` without a role in the list. */
30
+ option: `[role='option'], .${SUGGESTION_LIST_CLASS} > li:not([role])`,
31
+ empty: `.${SUGGESTION_EMPTY_CLASS}`,
32
+ status: "[role='status']",
33
+ /** The hidden field that receives the code of the option the user chose. */
34
+ value: `input.${SUGGESTION_VALUE_CLASS}`,
35
+ };
36
+ const CONTROL_SELECTOR = LT_SUGGESTION_PARTS.control;
37
+ const LIST_SELECTOR = LT_SUGGESTION_PARTS.list;
38
+ const OPTION_SELECTOR = LT_SUGGESTION_PARTS.option;
39
+ /**
40
+ * The keyboard and the filtering in a field with a suggestion list.
41
+ *
42
+ * The server writes the field and the list. When the markup is made with
43
+ * JavaScript, `lt.suggestion()` writes the roles, the ids and the wiring.
44
+ * When it comes from a template without JavaScript, the template writes
45
+ * `for` on the label and `id` on the field, or wraps the field in the label,
46
+ * which HTML requires for the field to have a name without JavaScript. The
47
+ * rest is enough: a `.lt-suggestion__list` with `<li>` and a
48
+ * `[role="status"]`. The web component sets the roles, gives the list and the
49
+ * options ids, connects the list to the field, and closes the list until the
50
+ * user touches the field. Only what is missing, so what the server wrote
51
+ * stays.
52
+ *
53
+ * The web component never writes an id on the field. Datastar and idiomorph
54
+ * recognise a node by its id, and a field with an id the server did not send
55
+ * is replaced on the next update, along with focus and what the user has
56
+ * typed.
57
+ *
58
+ * The field always shows the option's text. If the server has written a
59
+ * hidden `.lt-suggestion__value`, it receives the code from `data-value`, or
60
+ * the text when the option has none. Without JavaScript the text is
61
+ * submitted, and the server looks it up. The web component renders nothing.
62
+ * It used to create the whole field itself, and then there was neither a
63
+ * label nor an input before the script had run, and nothing was included in
64
+ * the submission.
65
+ *
66
+ * What the web component does is what the browser does not: it filters the
67
+ * options while the user types, moves the highlight with the arrow keys, and
68
+ * keeps `aria-activedescendant` in sync so the screen reader announces the
69
+ * option without focus leaving the field.
70
+ *
71
+ * If something else has already filtered, a server or a React component that
72
+ * renders only the matches, `prefiltered` says so, and the web component
73
+ * leaves the options alone.
74
+ *
75
+ * ```html
76
+ * <lt-suggestion>
77
+ * <label class="lt-label" for="city">City</label>
78
+ * <input class="lt-input" id="city" role="combobox" aria-controls="city-list"
79
+ * aria-expanded="false" aria-autocomplete="list" autocomplete="off"
80
+ * aria-describedby="city-status">
81
+ * <ul class="lt-suggestion__list" id="city-list" role="listbox" hidden>
82
+ * <li class="lt-suggestion__option" id="city-option-0" role="option">Bergen</li>
83
+ * </ul>
84
+ * <p class="lt-suggestion__empty">No matches</p>
85
+ * <span class="lt-sr-only" id="city-status" role="status"></span>
86
+ * </lt-suggestion>
87
+ * ```
88
+ */
89
+ export class LtSuggestion extends HostElement {
90
+ constructor() {
91
+ super(...arguments);
92
+ /** The options that already have their listener. */
93
+ this.bound = new WeakSet();
94
+ /** The ids the web component created, per node, so a patch gets the same ones back. */
95
+ this.ids = new WeakMap();
96
+ /** The roles the web component set. See `derivedParts`. */
97
+ this.roledByMe = new WeakSet();
98
+ /** True while the web component dispatches events, so it does not answer itself. */
99
+ this.choosing = false;
100
+ /** The `for` the web component wrote on the label. */
101
+ this.forByMe = new WeakMap();
102
+ /**
103
+ * The name the web component gave the list, per list: `aria-labelledby`
104
+ * pointing to the label, or `aria-label` with its text. If something else
105
+ * is there, the server wrote it, and it stays.
106
+ */
107
+ this.listNameByMe = new WeakMap();
108
+ /**
109
+ * Labels where the server wrote a `for` pointing to another id. Reported
110
+ * once; the memory is not reset, since the rewritten `for` cannot be told
111
+ * apart from a server that corrected itself.
112
+ */
113
+ this.wrongFor = new WeakSet();
114
+ /**
115
+ * Places the list right below the field, as wide as it. If there is no
116
+ * room below, it goes above, and it is squeezed into the viewport either
117
+ * way, like the panel in `<lt-popover>`. The position is physical,
118
+ * computed from the field's box.
119
+ */
120
+ this.reposition = () => {
121
+ const list = this.listElement;
122
+ const control = this.querySelector(CONTROL_SELECTOR);
123
+ if (!list || !control || !list.matches(":popover-open"))
124
+ return;
125
+ const field = control.getBoundingClientRect();
126
+ const height = list.getBoundingClientRect().height;
127
+ const space = 4;
128
+ let top = field.bottom + space;
129
+ if (top + height > window.innerHeight && field.top - height - space >= 0) {
130
+ top = field.top - height - space;
131
+ }
132
+ top = Math.max(space, Math.min(top, window.innerHeight - height - space));
133
+ list.style.setProperty("--lt-suggestion-top", `${Math.round(top)}px`);
134
+ list.style.setProperty("--lt-suggestion-left", `${Math.round(field.left)}px`);
135
+ list.style.setProperty("--lt-suggestion-width", `${Math.round(field.width)}px`);
136
+ };
137
+ /**
138
+ * The form is reset. The browser sets the text back to the server's after
139
+ * the event has been dispatched, so the code is set in the next turn, after
140
+ * the reset is done. The text there then decides: if it is the server's,
141
+ * the server's code applies; if it is the chosen one, the code stays;
142
+ * otherwise the code of the option with that text, otherwise none. The
143
+ * server's comes first because two options can have the same name: if the
144
+ * user chose the second Våler, the reset goes back to the server's. The
145
+ * server may also have sent the chosen one back, and then it is as much the
146
+ * server's word as the user's.
147
+ */
148
+ this.handleReset = () => {
149
+ setTimeout(() => {
150
+ const valueField = this.valueField;
151
+ if (!valueField || !this.control)
152
+ return;
153
+ const text = this.control.value;
154
+ const option = this.options.find((o) => (o.textContent ?? "").trim() === text);
155
+ const code = this.serverChoice && text === this.serverChoice.label
156
+ ? this.serverChoice.value
157
+ : this.chosen && text === this.chosen.label
158
+ ? this.chosen.value
159
+ : option
160
+ ? (option.getAttribute("data-value") ?? text)
161
+ : "";
162
+ this.writeCode(valueField, code);
163
+ this.chosen = code !== "" ? { label: text, value: code } : undefined;
164
+ }, 0);
165
+ };
166
+ this.handleInput = () => {
167
+ if (this.choosing)
168
+ return;
169
+ // The user changed the text after a choice. The code no longer applies.
170
+ // If the user types the chosen text again, after an update emptied both,
171
+ // the code comes back at once.
172
+ const valueField = this.valueField;
173
+ if (valueField && this.chosen) {
174
+ if (this.control?.value !== this.chosen.label) {
175
+ this.writeCode(valueField, "");
176
+ this.chosen = undefined;
177
+ }
178
+ else if (valueField.value === "") {
179
+ this.writeCode(valueField, this.chosen.value);
180
+ }
181
+ }
182
+ if (this.locked)
183
+ return;
184
+ this.filter();
185
+ this.setOpen(true);
186
+ };
187
+ this.handleFocus = () => {
188
+ if (this.locked)
189
+ return;
190
+ this.filter();
191
+ this.setOpen(true);
192
+ };
193
+ this.handleFocusOut = (event) => {
194
+ // If focus goes to something inside the web component, such as a button
195
+ // next to the field, the list stays.
196
+ const next = event.relatedTarget;
197
+ if (next instanceof Node && this.contains(next))
198
+ return;
199
+ this.setOpen(false);
200
+ };
201
+ this.handleKeydown = (event) => {
202
+ // During IME composition the keys belong to the composition. Enter
203
+ // confirms the characters, and must not choose the highlighted option.
204
+ if (event.isComposing || event.keyCode === 229)
205
+ return;
206
+ if (this.locked)
207
+ return;
208
+ const open = this.listElement?.hidden === false;
209
+ if (event.key === "ArrowDown") {
210
+ event.preventDefault();
211
+ if (!open) {
212
+ this.filter();
213
+ this.setOpen(true);
214
+ }
215
+ this.markActive(this.activeIndex + 1);
216
+ }
217
+ else if (event.key === "ArrowUp") {
218
+ event.preventDefault();
219
+ // The list must open first, as with arrow down. Without that
220
+ // `aria-activedescendant` pointed to an option in a list the field at the
221
+ // same time reported as closed, and the screen reader announced something
222
+ // that was not on screen.
223
+ if (!open) {
224
+ this.filter();
225
+ this.setOpen(true);
226
+ }
227
+ // If nothing is highlighted, arrow up goes to the last one.
228
+ // `activeIndex - 1` would have given the second to last, since nothing
229
+ // highlighted is -1.
230
+ this.markActive(this.activeIndex < 0 ? -1 : this.activeIndex - 1);
231
+ }
232
+ else if ((event.key === "PageUp" || event.key === "PageDown") &&
233
+ open &&
234
+ this.selectable.length > 0) {
235
+ // Home and End move the caret in the text, and are left alone. PgUp and
236
+ // PgDn go to the first and last option.
237
+ event.preventDefault();
238
+ this.markActive(event.key === "PageUp" ? 0 : this.selectable.length - 1);
239
+ }
240
+ else if (event.key === "Enter" && open && this.activeIndex >= 0) {
241
+ event.preventDefault();
242
+ const chosen = this.selectable[this.activeIndex];
243
+ if (chosen)
244
+ this.choose(chosen);
245
+ }
246
+ else if (event.key === "Escape" && open) {
247
+ event.preventDefault();
248
+ this.setOpen(false);
249
+ }
250
+ };
251
+ this.handleListMouseDown = (event) => {
252
+ event.preventDefault();
253
+ };
254
+ this.handleOptionMouseDown = (event) => {
255
+ // Keeps focus in the field. Without this the field lost focus before
256
+ // `click`, and `focusout` closed the list before the choice could happen.
257
+ event.preventDefault();
258
+ };
259
+ this.handleOptionClick = (event) => {
260
+ // The choice happens on `click`, which assistive technology also sends
261
+ // when it activates an element. It used to happen on `mousedown`, and a
262
+ // `click` alone chose nothing.
263
+ this.choose(event.currentTarget);
264
+ };
265
+ }
266
+ connectedCallback() {
267
+ /*
268
+ * The attributes are included, not just the children. Every write below
269
+ * compares first, or the observer would trigger itself forever.
270
+ */
271
+ this.observer = new MutationObserver(() => this.sync());
272
+ this.observer.observe(this, {
273
+ childList: true,
274
+ subtree: true,
275
+ attributes: true,
276
+ attributeFilter: [
277
+ "hidden",
278
+ "aria-expanded",
279
+ "aria-selected",
280
+ "aria-activedescendant",
281
+ // The wiring the web component fills in when the markup arrived without it.
282
+ "role",
283
+ "id",
284
+ "for",
285
+ "aria-controls",
286
+ "autocomplete",
287
+ "aria-autocomplete",
288
+ "class",
289
+ // The value field's `value` is an attribute, and a morph can write
290
+ // the server's empty value over the code the user chose.
291
+ "value",
292
+ // `form` on the field picks which form the reset belongs to.
293
+ "form",
294
+ // A field that becomes `readonly` or `disabled` must close the list.
295
+ "readonly",
296
+ "disabled",
297
+ "aria-disabled",
298
+ // The list sits in the top layer. A morph that removes `popover`
299
+ // closes the popover, and the web component sets it back.
300
+ "popover",
301
+ // The list's name, from the label.
302
+ "aria-labelledby",
303
+ "aria-label",
304
+ ],
305
+ });
306
+ this.sync();
307
+ window.addEventListener("resize", this.reposition);
308
+ window.addEventListener("scroll", this.reposition, true);
309
+ // The list closes when focus leaves the web component, with Tab as with
310
+ // anything else. On the host and not on the field: if focus goes through
311
+ // a button inside the web component and then out, the list must close
312
+ // then too. Without this it stayed over the next field, with
313
+ // `aria-expanded` true on a field without focus.
314
+ this.addEventListener("focusout", this.handleFocusOut);
315
+ }
316
+ attributeChangedCallback(name) {
317
+ if (name === SERVER_CONTROLLED && isServerControlled(this)) {
318
+ this.wantOpen = undefined;
319
+ this.active = undefined;
320
+ }
321
+ /*
322
+ * The attribute can come and go while the page is alive, and then the
323
+ * list must be corrected at once rather than on the next keystroke.
324
+ * `this.observer` signals that the web component is in the document: the
325
+ * callback also fires during the upgrade, before the children exist.
326
+ */
327
+ if (name === "prefiltered" && this.observer) {
328
+ // The count present when the attribute is set is the starting point,
329
+ // and nothing to announce. Without this the first change afterwards
330
+ // was taken as the starting point, and was lost.
331
+ this.announcedCount = this.prefiltered ? this.visible.length : undefined;
332
+ this.filter();
333
+ }
334
+ }
335
+ sync() {
336
+ this.wire();
337
+ this.bind();
338
+ this.repair();
339
+ this.announceFiltered();
340
+ this.mirrorPopover();
341
+ }
342
+ /**
343
+ * Opens and closes the popover the list sits in, following `hidden`.
344
+ *
345
+ * `hidden` still says whether the list is open, from the server as from
346
+ * the web component, and the popover follows. The list sits in the top
347
+ * layer, so a parent with `overflow`, such as the body of a dialog, does
348
+ * not clip it.
349
+ */
350
+ mirrorPopover() {
351
+ const list = this.listElement;
352
+ if (!list?.isConnected || !list.hasAttribute("popover"))
353
+ return;
354
+ const shown = list.matches(":popover-open");
355
+ try {
356
+ if (!list.hidden && !shown)
357
+ list.showPopover();
358
+ else if (list.hidden && shown)
359
+ list.hidePopover();
360
+ }
361
+ catch {
362
+ // An element on its way out of the tree cannot be shown. The next
363
+ // round sees it again.
364
+ return;
365
+ }
366
+ if (!list.hidden)
367
+ this.reposition();
368
+ }
369
+ /**
370
+ * Names the list from the label, when the markup arrived without a name.
371
+ *
372
+ * With `aria-labelledby` when the label is next to the field; the web
373
+ * component gives it an id when it has none, since it holds nothing the
374
+ * user can lose. If the label wraps the field, it gets no id: a morph would
375
+ * then replace the label, and the field with it. In that case the list
376
+ * gets the label's text as `aria-label`, without the field's value.
377
+ */
378
+ nameList(list, control) {
379
+ const mine = this.listNameByMe.get(list);
380
+ const labelledby = list.getAttribute("aria-labelledby");
381
+ const label = list.getAttribute("aria-label");
382
+ const fromServer = (labelledby !== null &&
383
+ !(mine?.name === "aria-labelledby" && mine.value === labelledby)) ||
384
+ (label !== null && !(mine?.name === "aria-label" && mine.value === label));
385
+ if (fromServer)
386
+ return;
387
+ const labelElement = control.labels?.[0] ?? this.querySelector("label");
388
+ if (!labelElement)
389
+ return;
390
+ if (labelElement.contains(control)) {
391
+ let text = "";
392
+ for (const node of labelElement.childNodes)
393
+ if (node !== control &&
394
+ !(node instanceof Element && node.contains(control)))
395
+ text += node.textContent ?? "";
396
+ text = text.replace(/\s+/g, " ").trim();
397
+ if (labelledby !== null)
398
+ setAttr(list, "aria-labelledby", null);
399
+ if (text === "") {
400
+ if (label !== null)
401
+ setAttr(list, "aria-label", null);
402
+ this.listNameByMe.delete(list);
403
+ return;
404
+ }
405
+ setAttr(list, "aria-label", text);
406
+ this.listNameByMe.set(list, { name: "aria-label", value: text });
407
+ return;
408
+ }
409
+ if (!labelElement.id) {
410
+ setAttr(labelElement, "id", this.rememberedId(labelElement, "lt-suggestion-label"));
411
+ }
412
+ if (label !== null)
413
+ setAttr(list, "aria-label", null);
414
+ setAttr(list, "aria-labelledby", labelElement.id);
415
+ this.listNameByMe.set(list, {
416
+ name: "aria-labelledby",
417
+ value: labelElement.id,
418
+ });
419
+ }
420
+ /** The id an element should have: the one it has, otherwise the one the web component created last. */
421
+ rememberedId(element, prefix) {
422
+ if (element.id)
423
+ return element.id;
424
+ let id = this.ids.get(element);
425
+ if (!id) {
426
+ id = uniqueId(prefix);
427
+ this.ids.set(element, id);
428
+ }
429
+ return id;
430
+ }
431
+ /**
432
+ * Fills in the wiring where the markup arrived without it. Only what is
433
+ * missing.
434
+ *
435
+ * The list is closed when the markup says nothing about it, that is when
436
+ * neither `aria-expanded` on the field nor `hidden` on the list is there.
437
+ * If the markup states one of them, that is the server's word on both, as
438
+ * with `lt.suggestion({ open: true })`.
439
+ */
440
+ wire() {
441
+ const control = this.querySelector(CONTROL_SELECTOR);
442
+ if (!control)
443
+ return;
444
+ if (!control.hasAttribute("role"))
445
+ setAttr(control, "role", "combobox");
446
+ if (!control.hasAttribute("autocomplete")) {
447
+ setAttr(control, "autocomplete", "off");
448
+ }
449
+ if (!control.hasAttribute("aria-autocomplete")) {
450
+ setAttr(control, "aria-autocomplete", "list");
451
+ }
452
+ const label = this.querySelector("label");
453
+ const root = this.getRootNode();
454
+ if (label) {
455
+ addClass(label, LABEL_CLASS);
456
+ // `for` and `id` are the same information, and are written from the
457
+ // field's id, never the other way round: the web component does not
458
+ // write an id on the field. If the label wraps the field, the wiring
459
+ // is implicit. Same rule as in `<lt-field>`.
460
+ if (control.id && !label.contains(control)) {
461
+ const current = label.getAttribute("for");
462
+ if (current !== null &&
463
+ current !== control.id &&
464
+ current !== this.forByMe.get(label)) {
465
+ this.wrongFor.add(label);
466
+ }
467
+ setAttr(label, "for", control.id);
468
+ this.forByMe.set(label, control.id);
469
+ }
470
+ warnAboutMarkup(this, "for on the label pointed to a different id than the field's. " +
471
+ "The component rewrote it, but without JavaScript the field has " +
472
+ "no label and a screen reader announces it without a name, so " +
473
+ "write the field's id in for in the template.", () => {
474
+ const now = this.querySelector(CONTROL_SELECTOR);
475
+ const labelNow = this.querySelector("label");
476
+ return (now !== null && labelNow !== null && this.wrongFor.has(labelNow));
477
+ });
478
+ warnAboutMarkup(this, "<label> is not connected to the field: the field has no id, " +
479
+ "and the label does not wrap it, so without JavaScript a screen " +
480
+ "reader announces the field without a name. Write an id on the " +
481
+ "field and the same value in for on the label, or wrap the " +
482
+ "field in the <label>; the component does not write the id " +
483
+ "itself, because an id the server did not send makes a server " +
484
+ "update replace the field.", () => {
485
+ const now = this.querySelector(CONTROL_SELECTOR);
486
+ const labelNow = this.querySelector("label");
487
+ return (now !== null &&
488
+ labelNow !== null &&
489
+ !now.id &&
490
+ !labelNow.contains(now) &&
491
+ !now.hasAttribute("aria-label") &&
492
+ !now.hasAttribute("aria-labelledby"));
493
+ });
494
+ // `for` wins over the label wrapping the field. If it points to another
495
+ // id, the field has no label, with JavaScript as well.
496
+ warnAboutMarkup(this, "<label> wraps the field, but its for points to a different id " +
497
+ "than the field's, and for wins, so a screen reader announces " +
498
+ "the field without a name. Remove for, or give the field the " +
499
+ "same id.", () => {
500
+ const now = this.querySelector(CONTROL_SELECTOR);
501
+ const labelNow = this.querySelector("label");
502
+ if (!now || !labelNow?.contains(now))
503
+ return false;
504
+ const target = labelNow.getAttribute("for");
505
+ return target !== null && target !== now.id;
506
+ });
507
+ }
508
+ else {
509
+ // With no label inside, it can be outside, with `for` set to the field's
510
+ // id. It gets the class, as in `<lt-field>`; a patch of this region does
511
+ // not touch it, so it needs no repair.
512
+ if (control.id) {
513
+ const outside = this.getRootNode().querySelector?.(`label[for="${CSS.escape(control.id)}"]`);
514
+ if (outside)
515
+ addClass(outside, LABEL_CLASS);
516
+ }
517
+ warnAboutMarkup(this, "found no <label>, so the field has no label and a screen " +
518
+ "reader announces it without a name. If the label is outside " +
519
+ "<lt-suggestion>, its for must point to the field's id.", () => {
520
+ const now = this.querySelector(CONTROL_SELECTOR);
521
+ if (!now || this.querySelector("label"))
522
+ return false;
523
+ // A <label> around the whole <lt-suggestion> also names the field.
524
+ if ((now.labels?.length ?? 0) > 0)
525
+ return false;
526
+ if (now.hasAttribute("aria-label") ||
527
+ now.hasAttribute("aria-labelledby"))
528
+ return false;
529
+ const rootNow = this.getRootNode();
530
+ return !(now.id &&
531
+ rootNow.querySelector?.(`label[for="${CSS.escape(now.id)}"]`));
532
+ });
533
+ }
534
+ const list = this.listElement;
535
+ if (list) {
536
+ addClass(list, SUGGESTION_LIST_CLASS);
537
+ if (!list.hasAttribute("role"))
538
+ setAttr(list, "role", "listbox");
539
+ if (!list.id) {
540
+ // Same rule as for the field: if `aria-controls` points to nothing,
541
+ // that is the id the new list should have.
542
+ const wanted = control.getAttribute("aria-controls");
543
+ const free = wanted && !root.getElementById?.(wanted);
544
+ if (free)
545
+ this.ids.set(list, wanted);
546
+ setAttr(list, "id", this.rememberedId(list, "lt-suggestion-list"));
547
+ }
548
+ if (!control.hasAttribute("aria-controls")) {
549
+ setAttr(control, "aria-controls", list.id);
550
+ }
551
+ if (!list.hasAttribute("popover"))
552
+ setAttr(list, "popover", "manual");
553
+ this.nameList(list, control);
554
+ if (!control.hasAttribute("aria-expanded")) {
555
+ if (!list.hasAttribute("hidden"))
556
+ setFlag(list, "hidden", true);
557
+ setAttr(control, "aria-expanded", "false");
558
+ }
559
+ }
560
+ for (const option of this.options) {
561
+ addClass(option, SUGGESTION_OPTION_CLASS);
562
+ if (!option.hasAttribute("role")) {
563
+ setAttr(option, "role", "option");
564
+ this.roledByMe.add(option);
565
+ }
566
+ if (!option.id) {
567
+ setAttr(option, "id", this.rememberedId(option, "lt-suggestion-option"));
568
+ }
569
+ // From the start, not only when the list closes: otherwise the options
570
+ // the server sent were without it until then, while the rest of the list
571
+ // had it.
572
+ if (!option.hasAttribute("aria-selected")) {
573
+ setAttr(option, "aria-selected", "false");
574
+ }
575
+ }
576
+ }
577
+ /**
578
+ * Sets the user's state back after a patch.
579
+ *
580
+ * The filtering is recomputed from what is in the field, so it needs no
581
+ * memory. What the list shows and what is highlighted do.
582
+ */
583
+ repair() {
584
+ if (!this.control)
585
+ return;
586
+ // The code is set back under `server-controlled` too. The attribute is
587
+ // about whether the list is open and what is highlighted, not what the user
588
+ // chose.
589
+ const valueField = this.valueField;
590
+ if (valueField &&
591
+ this.chosen &&
592
+ this.control.value === this.chosen.label &&
593
+ valueField.value === "") {
594
+ this.writeCode(valueField, this.chosen.value);
595
+ }
596
+ // An emptied status element gets its text back while the list is open,
597
+ // where the count is what the user is working on. Not otherwise: every
598
+ // patch of the region would announce the old count again. And not when
599
+ // something else filters: then a patch is the answer to a keystroke, and
600
+ // `announceFiltered` announces the new count right after.
601
+ const status = this.querySelector(LT_SUGGESTION_PARTS.status);
602
+ if (status &&
603
+ this.wantOpen === true &&
604
+ !this.prefiltered &&
605
+ this.announcedText !== undefined &&
606
+ (status.textContent ?? "").trim() === "") {
607
+ setText(status, this.announcedText);
608
+ }
609
+ if (isServerControlled(this))
610
+ return;
611
+ // A field that became `readonly` or `disabled` while the list was open
612
+ // closes it. Otherwise the repair kept it open, and Escape does nothing in
613
+ // a locked field.
614
+ if (this.locked && this.wantOpen)
615
+ this.setOpen(false);
616
+ if (this.wantOpen !== undefined) {
617
+ /*
618
+ * Each half is checked separately. A patch that touched only the field left
619
+ * `aria-expanded="false"` while the options were visible, and the screen
620
+ * reader said the list was closed while it was on screen.
621
+ */
622
+ const list = this.listElement;
623
+ const expanded = this.control.getAttribute("aria-expanded");
624
+ if ((list && list.hidden === this.wantOpen) ||
625
+ expanded !== String(this.wantOpen)) {
626
+ this.applyOpen(this.wantOpen);
627
+ }
628
+ if (this.wantOpen)
629
+ this.filter();
630
+ }
631
+ if (this.active) {
632
+ /*
633
+ * The text is what counts, and the option must be visible. Otherwise the
634
+ * web component highlighted something the server had just filtered out, and
635
+ * `aria-activedescendant` pointed to a hidden element. The id is a
636
+ * fallback: if the server sends the list again, the options are replaced
637
+ * and get new ids, and the highlight is found again by its text. The code
638
+ * counts too, since two places can have the same name.
639
+ */
640
+ const sameText = (o) => (o.textContent ?? "").trim() === this.active?.label &&
641
+ o.getAttribute("data-value") === this.active?.value;
642
+ const option = this.visible.find((o) => o.id === this.active?.id && sameText(o)) ??
643
+ this.visible.find(sameText);
644
+ if (!option) {
645
+ // The pointer must go together with the highlight. Without this
646
+ // `aria-activedescendant` pointed to an option that is now called
647
+ // something else, and the screen reader announced a place the user never
648
+ // navigated to.
649
+ this.active = undefined;
650
+ this.control.removeAttribute("aria-activedescendant");
651
+ }
652
+ else if (option.getAttribute("aria-selected") !== "true" ||
653
+ this.control.getAttribute("aria-activedescendant") !== option.id) {
654
+ // Both sides of the wiring are checked. If the patch stripped only
655
+ // `aria-activedescendant` while the highlight remained, the screen reader
656
+ // lost its reading position without anything else looking wrong.
657
+ this.markOption(option);
658
+ }
659
+ }
660
+ }
661
+ disconnectedCallback() {
662
+ window.removeEventListener("resize", this.reposition);
663
+ window.removeEventListener("scroll", this.reposition, true);
664
+ this.removeEventListener("focusout", this.handleFocusOut);
665
+ this.observer?.disconnect();
666
+ this.observer = undefined;
667
+ this.unbind();
668
+ }
669
+ /**
670
+ * Something else has already filtered, so the web component leaves the
671
+ * options alone.
672
+ *
673
+ * The name used to be `server-filtered`, and that was misleading: it is not
674
+ * about servers. A React app that renders only the matches has filtered all
675
+ * the same, without any server involved.
676
+ *
677
+ * The web component hides an option when its text does not contain what is
678
+ * in the field. If you filter on something else, without diacritics, on a
679
+ * code that is not shown, or fuzzily, the two disagree, and then your filter
680
+ * should win. The attribute turns off both the hiding and the empty
681
+ * message, that is everything about what the list shows. The announcement
682
+ * of the number of matches remains, since it is not markup.
683
+ */
684
+ get prefiltered() {
685
+ return this.hasAttribute("prefiltered");
686
+ }
687
+ set prefiltered(on) {
688
+ /*
689
+ * The setter is not decoration. React 19 writes the property rather than
690
+ * the attribute when a web component has one with that name, and a getter
691
+ * alone throws "Cannot set property prefiltered". The attribute never
692
+ * landed, and the web component hid what React had just rendered. The
693
+ * hyphen in the old name hid the problem: `server-filtered` cannot be a
694
+ * property name, so React sent it as an attribute.
695
+ */
696
+ setFlag(this, "prefiltered", on);
697
+ }
698
+ get listElement() {
699
+ return this.querySelector(LIST_SELECTOR);
700
+ }
701
+ get emptyElement() {
702
+ return this.querySelector(LT_SUGGESTION_PARTS.empty);
703
+ }
704
+ get valueField() {
705
+ return this.querySelector(LT_SUGGESTION_PARTS.value);
706
+ }
707
+ /** The field accepts no input: `readonly` or `disabled`. */
708
+ get locked() {
709
+ return Boolean(this.control?.readOnly || this.control?.disabled);
710
+ }
711
+ /**
712
+ * The options, recognised by the role and not by the class.
713
+ *
714
+ * The role must be there anyway, for the screen reader. A template that
715
+ * wrote `<li role="option">` with its own styling got neither filtering nor
716
+ * arrow keys when the web component looked for the class, and nothing said
717
+ * so.
718
+ */
719
+ get options() {
720
+ return derivedParts(this, OPTION_SELECTOR, "option", this.roledByMe);
721
+ }
722
+ /**
723
+ * Which visible option is highlighted now.
724
+ *
725
+ * It is read from `aria-selected` in the markup, not from a counter inside
726
+ * the web component. The server can set the highlight itself with
727
+ * `lt.suggestion({ activeIndex })`, and a parallel counter would have jumped
728
+ * to the top of the list on the first arrow key instead of to the next
729
+ * option. The filtering also moves the options around, and then the lookup
730
+ * gives the right answer by itself.
731
+ */
732
+ get activeIndex() {
733
+ return this.selectable.findIndex((option) => option.getAttribute("aria-selected") === "true");
734
+ }
735
+ /** The options that are visible now. */
736
+ get visible() {
737
+ return this.options.filter((option) => !option.hidden);
738
+ }
739
+ /**
740
+ * The visible options that can be chosen. An option with
741
+ * `aria-disabled="true"` is shown, but the arrow keys skip it and it cannot
742
+ * be chosen, with neither keyboard nor mouse.
743
+ */
744
+ get selectable() {
745
+ return this.visible.filter((option) => option.getAttribute("aria-disabled") !== "true");
746
+ }
747
+ unbind() {
748
+ const control = this.control;
749
+ if (control) {
750
+ control.removeEventListener("input", this.handleInput);
751
+ control.removeEventListener("keydown", this.handleKeydown);
752
+ control.removeEventListener("focus", this.handleFocus);
753
+ this.control = undefined;
754
+ }
755
+ this.boundForm?.removeEventListener("reset", this.handleReset);
756
+ this.boundForm = undefined;
757
+ this.list?.removeEventListener("mousedown", this.handleListMouseDown);
758
+ this.list = undefined;
759
+ this.empty?.removeEventListener("mousedown", this.handleListMouseDown);
760
+ this.empty = undefined;
761
+ }
762
+ /**
763
+ * Reports when an update replaced the field because the id from the server
764
+ * changed.
765
+ *
766
+ * The message comes only when the user had something to lose: the old field
767
+ * is out of the tree, both had an id from the server, the ids differ, `name`
768
+ * is the same, and the old field held something other than the new one. A
769
+ * server that sends the text back has not lost it.
770
+ */
771
+ warnAboutReplacedControl(lost, control) {
772
+ if (lost.isConnected || !lost.id || !control.id || lost.id === control.id)
773
+ return;
774
+ if (lost.getAttribute("name") !== control.getAttribute("name"))
775
+ return;
776
+ if (lost.value === control.value)
777
+ return;
778
+ warnAboutMarkup(this, "a server update replaced the field and lost what the user had " +
779
+ "typed, along with focus and the list, because the field's id " +
780
+ "was different from last time and the update did not recognise " +
781
+ "it. Build the id from something that is the same every time, " +
782
+ "such as the field name or a key in the data, not from a random " +
783
+ "number or a counter.",
784
+ // The replacement has happened and cannot undo itself, unless another
785
+ // update has already replaced the field again.
786
+ () => !lost.isConnected &&
787
+ this.querySelector(CONTROL_SELECTOR) === control);
788
+ }
789
+ bind() {
790
+ const control = this.querySelector(CONTROL_SELECTOR);
791
+ if (!control) {
792
+ warnAboutMarkup(this, 'found no field: neither an <input> nor a [role="combobox"]. ' +
793
+ "Without one the component does not know which field to listen " +
794
+ "to, and neither filtering nor the arrow keys work.",
795
+ // An empty element is a region the server has not filled yet, and not
796
+ // an error in the markup.
797
+ () => this.childElementCount > 0 &&
798
+ this.querySelector(CONTROL_SELECTOR) === null);
799
+ // The field is gone. Firefox sends no `focusout` when the focused node is
800
+ // removed, so the list would otherwise stay open without a field.
801
+ this.wantOpen = undefined;
802
+ this.active = undefined;
803
+ const orphan = this.listElement;
804
+ if (orphan)
805
+ setFlag(orphan, "hidden", true);
806
+ return;
807
+ }
808
+ warnAboutMarkup(this, 'found no list: neither a [role="listbox"] nor a ' +
809
+ ".lt-suggestion__list. The options cannot be shown, filtered or " +
810
+ "chosen with the keyboard.", () => this.querySelector(CONTROL_SELECTOR) !== null && !this.listElement);
811
+ // The number of matches is announced in a status element the server
812
+ // sends. Without it, people who cannot see the screen get no word that the
813
+ // list narrowed, and the web component used to stay silent then.
814
+ warnAboutMarkup(this, 'found no [role="status"], so the number of matches is not ' +
815
+ "announced to people who cannot see the screen. Write an empty " +
816
+ "element with that role, as `lt.suggestion()` does.", () => this.querySelector(CONTROL_SELECTOR) !== null &&
817
+ this.listElement !== null &&
818
+ this.querySelector(LT_SUGGESTION_PARTS.status) === null);
819
+ if (control !== this.control) {
820
+ if (this.control)
821
+ this.warnAboutReplacedControl(this.control, control);
822
+ // The new field has no focus. The open list and the highlight belonged to
823
+ // the old one.
824
+ this.wantOpen = undefined;
825
+ this.active = undefined;
826
+ // A value field the server filled applies to the text in the field now.
827
+ const valueField = this.valueField;
828
+ this.serverChoice =
829
+ valueField && valueField.value !== ""
830
+ ? { label: control.value, value: valueField.value }
831
+ : undefined;
832
+ this.chosen = this.serverChoice;
833
+ this.control?.removeEventListener("input", this.handleInput);
834
+ this.control?.removeEventListener("keydown", this.handleKeydown);
835
+ this.control?.removeEventListener("focus", this.handleFocus);
836
+ control.addEventListener("input", this.handleInput);
837
+ control.addEventListener("keydown", this.handleKeydown);
838
+ control.addEventListener("focus", this.handleFocus);
839
+ this.control = control;
840
+ }
841
+ // The form is read every time: `form` on the field can point to another
842
+ // one without anything moving.
843
+ const form = control.form;
844
+ if (form !== this.boundForm) {
845
+ this.boundForm?.removeEventListener("reset", this.handleReset);
846
+ form?.addEventListener("reset", this.handleReset);
847
+ this.boundForm = form;
848
+ }
849
+ const code = this.valueField?.value ?? "";
850
+ if (code !== "" &&
851
+ code !== this.writtenCode &&
852
+ code !== this.chosen?.value) {
853
+ // Something other than the web component wrote the code: the server's word.
854
+ this.serverChoice = { label: control.value, value: code };
855
+ this.chosen = this.serverChoice;
856
+ }
857
+ /*
858
+ * A press in the list or on the empty message must not take focus from the
859
+ * field, not on the scrollbar either. Otherwise `focusout` closed the list
860
+ * before the choice could happen. Both can be replaced by a patch, so the
861
+ * listeners follow the elements.
862
+ */
863
+ const list = this.listElement;
864
+ if (list !== this.list) {
865
+ this.list?.removeEventListener("mousedown", this.handleListMouseDown);
866
+ list?.addEventListener("mousedown", this.handleListMouseDown);
867
+ this.list = list ?? undefined;
868
+ }
869
+ const empty = this.emptyElement;
870
+ if (empty !== this.empty) {
871
+ this.empty?.removeEventListener("mousedown", this.handleListMouseDown);
872
+ empty?.addEventListener("mousedown", this.handleListMouseDown);
873
+ this.empty = empty ?? undefined;
874
+ }
875
+ // The options are replaced independently of the field: in a Datastar app
876
+ // the server sends a new list while the user types. Had this been behind
877
+ // the check above, the new options would never have got their listener,
878
+ // and choosing with the mouse would have stopped working after the first
879
+ // update.
880
+ for (const option of this.options) {
881
+ if (this.bound.has(option))
882
+ continue;
883
+ option.addEventListener("mousedown", this.handleOptionMouseDown);
884
+ option.addEventListener("click", this.handleOptionClick);
885
+ this.bound.add(option);
886
+ }
887
+ }
888
+ setOpen(open) {
889
+ /*
890
+ * A list without options below an empty field has nothing to show, and is
891
+ * not opened. Without this, focus opened an empty list, and a field waiting
892
+ * for its first search showed "No matches" before the user had typed a
893
+ * character: the empty message follows the list, and the list was open.
894
+ */
895
+ const show = open && this.hasSomethingToShow();
896
+ this.wantOpen = show;
897
+ this.applyOpen(show);
898
+ }
899
+ hasSomethingToShow() {
900
+ return this.options.length > 0 || (this.control?.value ?? "").trim() !== "";
901
+ }
902
+ /** Writes the state into the markup, without changing what the user wanted. */
903
+ applyOpen(open) {
904
+ const list = this.listElement;
905
+ if (!list || !this.control)
906
+ return;
907
+ setFlag(list, "hidden", !open);
908
+ setAttr(this.control, "aria-expanded", String(open));
909
+ this.syncEmpty();
910
+ if (!open) {
911
+ this.active = undefined;
912
+ this.control.removeAttribute("aria-activedescendant");
913
+ for (const option of this.options) {
914
+ setAttr(option, "aria-selected", "false");
915
+ }
916
+ }
917
+ }
918
+ /**
919
+ * The empty message follows the list: visible only when the list is open and
920
+ * empty.
921
+ *
922
+ * It is a sibling of the list and not a child, so `hidden` on the list did
923
+ * not hide it, and "No matches" stayed below a closed field after Escape.
924
+ * If something else has filtered, it also controls the empty message.
925
+ */
926
+ syncEmpty() {
927
+ if (this.prefiltered)
928
+ return;
929
+ const empty = this.emptyElement;
930
+ const list = this.listElement;
931
+ if (!empty || !list)
932
+ return;
933
+ setFlag(empty, "hidden", Boolean(list.hidden) || this.visible.length > 0);
934
+ }
935
+ /**
936
+ * Hides what does not match, and announces how many are left.
937
+ *
938
+ * Runs when the user types or focuses the field, and again after a patch
939
+ * while the list is open, since the patch may have replaced the options.
940
+ */
941
+ filter() {
942
+ /*
943
+ * If something else has filtered, it decides what the list shows, and the
944
+ * empty message is part of that. The web component therefore touches
945
+ * nothing here, and the announcement comes from `announceFiltered`
946
+ * instead.
947
+ *
948
+ * Two versions before this one were wrong. The first returned at once, and
949
+ * then a Datastar app had no announcement of the number of matches at all.
950
+ * The second counted here: then an empty field announced "No matches"
951
+ * already on focus, before the user had typed a character, and an
952
+ * asynchronous search announced it again on every keystroke while the
953
+ * answer was still on its way. The web component also made the empty
954
+ * message visible again after the app had hidden it, and there was no way
955
+ * around it.
956
+ */
957
+ if (this.prefiltered)
958
+ return;
959
+ const query = (this.control?.value ?? "").trim().toLowerCase();
960
+ for (const option of this.options) {
961
+ const label = (option.textContent ?? "").trim().toLowerCase();
962
+ setFlag(option, "hidden", query !== "" && !label.includes(query));
963
+ }
964
+ this.dropHiddenMark();
965
+ this.syncEmpty();
966
+ this.announce(this.visible.length);
967
+ }
968
+ /**
969
+ * Removes the highlight from an option the filtering just hid.
970
+ *
971
+ * The cleanup belongs here, where the hiding happens, and not only in
972
+ * `repair()`: that returns at once under `server-controlled`, and then
973
+ * `aria-activedescendant` pointed to a hidden option, which the screen
974
+ * reader announced anyway.
975
+ */
976
+ dropHiddenMark() {
977
+ const control = this.control;
978
+ if (!control)
979
+ return;
980
+ const marked = this.options.find((option) => option.getAttribute("aria-selected") === "true");
981
+ if (marked?.hidden) {
982
+ setAttr(marked, "aria-selected", "false");
983
+ if (this.active?.id === marked.id)
984
+ this.active = undefined;
985
+ }
986
+ const pointed = control.getAttribute("aria-activedescendant");
987
+ if (pointed) {
988
+ const target = this.options.find((option) => option.id === pointed);
989
+ if (!target || target.hidden) {
990
+ control.removeAttribute("aria-activedescendant");
991
+ }
992
+ }
993
+ }
994
+ /**
995
+ * Announces the count when something else has filtered.
996
+ *
997
+ * The list then changes not from a keystroke but from the app rendering
998
+ * again, and that is exactly the change the observer sees. The message
999
+ * belongs here and not in `filter()`, because half a second over the
1000
+ * network can pass between the keystroke and the new answer.
1001
+ *
1002
+ * Only changes are announced. The first count is the starting point and not
1003
+ * news, and it is often 0 because the app has not rendered the list yet.
1004
+ */
1005
+ announceFiltered() {
1006
+ if (!this.prefiltered)
1007
+ return;
1008
+ const hits = this.visible.length;
1009
+ if (hits === this.announcedCount)
1010
+ return;
1011
+ const first = this.announcedCount === undefined;
1012
+ this.announcedCount = hits;
1013
+ if (!first)
1014
+ this.announce(hits);
1015
+ }
1016
+ /**
1017
+ * Announces the number of matches.
1018
+ *
1019
+ * Without this, people who cannot see the screen get no word that the list
1020
+ * narrowed while they typed. The text is generated on the client, and
1021
+ * `repair()` writes it back when an update empties the element. If the
1022
+ * element is missing, `bind()` has already reported it.
1023
+ */
1024
+ announce(hits) {
1025
+ const status = this.querySelector(LT_SUGGESTION_PARTS.status);
1026
+ if (!status)
1027
+ return;
1028
+ this.announcedText = this.countText(hits);
1029
+ setText(status, this.announcedText);
1030
+ }
1031
+ /**
1032
+ * The text for a count, in the page's language.
1033
+ *
1034
+ * `count-none` applies to zero matches, since "No matches" is a sentence of
1035
+ * its own and not an inflection. Otherwise `Intl.PluralRules` picks the form
1036
+ * the language requires, and `count-<form>` gives the text, with `{n}` for
1037
+ * the number. If the form is missing, `count-other` applies. If that is
1038
+ * missing too, the default text from `DEFAULT_TEXTS` applies.
1039
+ */
1040
+ countText(hits) {
1041
+ const lang = languageOf(this);
1042
+ const written = (name) => this.getAttribute(name);
1043
+ const form = new Intl.PluralRules(lang).select(hits);
1044
+ const template = (hits === 0 ? written("count-none") : null) ??
1045
+ written(`count-${form}`) ??
1046
+ written("count-other");
1047
+ const number = new Intl.NumberFormat(lang).format(hits);
1048
+ if (template !== null)
1049
+ return template.replaceAll("{n}", number);
1050
+ const fallback = hits === 0
1051
+ ? DEFAULT_TEXTS.suggestionNone
1052
+ : hits === 1
1053
+ ? DEFAULT_TEXTS.suggestionOne
1054
+ : DEFAULT_TEXTS.suggestionOther;
1055
+ return fallback.replaceAll("{n}", number);
1056
+ }
1057
+ markActive(index) {
1058
+ const shown = this.selectable;
1059
+ if (shown.length === 0 || !this.control)
1060
+ return;
1061
+ const next = (index + shown.length) % shown.length;
1062
+ this.markOption(shown[next]);
1063
+ shown[next].scrollIntoView({ block: "nearest" });
1064
+ }
1065
+ /** Highlights one option, and remembers which. */
1066
+ markOption(option) {
1067
+ if (!this.control)
1068
+ return;
1069
+ for (const other of this.options) {
1070
+ setAttr(other, "aria-selected", other === option ? "true" : "false");
1071
+ }
1072
+ this.active = {
1073
+ id: option.id,
1074
+ label: (option.textContent ?? "").trim(),
1075
+ value: option.getAttribute("data-value"),
1076
+ };
1077
+ // aria-activedescendant moves the screen reader's reading position without
1078
+ // focus leaving the field. Without it the option is never announced.
1079
+ setAttr(this.control, "aria-activedescendant", option.id);
1080
+ }
1081
+ choose(option) {
1082
+ if (!this.control || this.locked)
1083
+ return;
1084
+ if (option.getAttribute("aria-disabled") === "true")
1085
+ return;
1086
+ const label = (option.textContent ?? "").trim();
1087
+ const value = option.getAttribute("data-value") ?? label;
1088
+ /*
1089
+ * The setter from the prototype chain, past a `value` React has put on the
1090
+ * node itself to track it. Set directly on the node, React saw no change,
1091
+ * `onChange` never came, and on the next render React wrote the old text
1092
+ * back into the field. The chain and not just the nearest prototype, so
1093
+ * `<textarea>` and a subclass of `<input>` work too.
1094
+ */
1095
+ let setter;
1096
+ for (let proto = Object.getPrototypeOf(this.control); proto && !setter; proto = Object.getPrototypeOf(proto)) {
1097
+ setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
1098
+ }
1099
+ // The field shows the text. The code goes to the value field, when the
1100
+ // server has written one, and in the event. `data-value` used to end up in
1101
+ // the field, and the user who chose Bergen saw "4601".
1102
+ if (setter)
1103
+ setter.call(this.control, label);
1104
+ else
1105
+ this.control.value = label;
1106
+ const valueField = this.valueField;
1107
+ if (valueField) {
1108
+ this.writeCode(valueField, value);
1109
+ this.chosen = { label, value };
1110
+ }
1111
+ // The flag must be set before the events are dispatched. `input` is the
1112
+ // same event the web component listens to, so without this the list opens
1113
+ // again the moment the user has chosen something.
1114
+ this.choosing = true;
1115
+ this.control.dispatchEvent(new Event("input", { bubbles: true }));
1116
+ this.control.dispatchEvent(new Event("change", { bubbles: true }));
1117
+ this.choosing = false;
1118
+ this.setOpen(false);
1119
+ this.dispatchEvent(new CustomEvent("suggestion-select", {
1120
+ detail: { value, label },
1121
+ bubbles: true,
1122
+ composed: true,
1123
+ }));
1124
+ }
1125
+ /**
1126
+ * Writes the code in the value field and dispatches `input` and `change` on
1127
+ * it, so `data-bind` in Datastar sees the code. The events do not bubble: a
1128
+ * `data-on:input` on the host, which the recipe uses to fetch suggestions,
1129
+ * must not trigger a search for the code every time the user chooses.
1130
+ */
1131
+ writeCode(valueField, code) {
1132
+ if (valueField.value === code)
1133
+ return;
1134
+ this.writtenCode = code;
1135
+ setAttr(valueField, "value", code);
1136
+ // A hidden field reads the attribute. If the field has another type and a
1137
+ // morph has already set the property, the attribute no longer counts.
1138
+ if (valueField.value !== code)
1139
+ valueField.value = code;
1140
+ valueField.dispatchEvent(new Event("input"));
1141
+ valueField.dispatchEvent(new Event("change"));
1142
+ }
1143
+ }
1144
+ LtSuggestion.observedAttributes = [
1145
+ "prefiltered",
1146
+ SERVER_CONTROLLED,
1147
+ ...COUNT_ATTRIBUTES,
1148
+ ];
1149
+ export function defineLtSuggestion(tagName = LT_SUGGESTION_TAG) {
1150
+ defineElement(tagName, LtSuggestion);
1151
+ }
1152
+ //# sourceMappingURL=lt-suggestion.js.map