@shortfuse/materialdesignweb 0.8.0 → 0.9.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 (405) hide show
  1. package/README.md +50 -198
  2. package/bin/mdw-css.js +1 -1
  3. package/components/Badge.js +15 -5
  4. package/components/Body.js +7 -0
  5. package/components/BottomAppBar.js +7 -10
  6. package/components/BottomSheet.js +472 -0
  7. package/components/Box.js +11 -49
  8. package/components/Button.js +81 -82
  9. package/components/Card.js +74 -62
  10. package/components/Checkbox.js +15 -25
  11. package/components/CheckboxIcon.js +19 -31
  12. package/components/Chip.js +18 -13
  13. package/components/Dialog.js +70 -100
  14. package/components/DialogActions.js +4 -0
  15. package/components/Display.js +64 -0
  16. package/components/Divider.js +5 -0
  17. package/components/Fab.js +94 -17
  18. package/components/FabContainer.js +57 -0
  19. package/components/FilterChip.js +43 -32
  20. package/components/Grid.js +187 -0
  21. package/components/Headline.js +9 -28
  22. package/components/Icon.js +80 -71
  23. package/components/IconButton.js +77 -120
  24. package/components/Input.js +745 -86
  25. package/components/InputChip.js +193 -0
  26. package/components/Label.js +7 -0
  27. package/components/List.js +11 -5
  28. package/components/ListItem.js +92 -23
  29. package/components/ListOption.js +143 -65
  30. package/components/Listbox.js +57 -17
  31. package/components/Menu.js +39 -27
  32. package/components/MenuItem.js +49 -36
  33. package/components/NavBar.js +66 -21
  34. package/components/NavBarItem.js +5 -0
  35. package/components/NavDrawer.js +33 -16
  36. package/components/NavDrawerItem.js +7 -4
  37. package/components/NavItem.js +61 -34
  38. package/components/NavRail.js +32 -21
  39. package/components/NavRailItem.js +10 -2
  40. package/components/Page.js +119 -0
  41. package/components/Pane.js +24 -0
  42. package/components/Popup.js +23 -8
  43. package/components/Progress.js +25 -5
  44. package/components/Radio.js +8 -7
  45. package/components/RadioIcon.js +24 -15
  46. package/components/Ripple.js +25 -7
  47. package/components/Root.js +225 -0
  48. package/components/Scrim.js +95 -0
  49. package/components/Search.js +30 -25
  50. package/components/SegmentedButton.js +53 -40
  51. package/components/SegmentedButtonGroup.js +15 -12
  52. package/components/Select.js +19 -10
  53. package/components/Shape.js +10 -66
  54. package/components/SideSheet.js +337 -0
  55. package/components/Slider.js +93 -36
  56. package/components/Snackbar.js +52 -20
  57. package/components/SnackbarContainer.js +51 -0
  58. package/components/Surface.js +20 -10
  59. package/components/Switch.js +21 -18
  60. package/components/SwitchIcon.js +62 -33
  61. package/components/Tab.js +78 -38
  62. package/components/TabContent.js +33 -12
  63. package/components/TabList.js +95 -34
  64. package/components/TabPanel.js +10 -1
  65. package/components/Table.js +151 -0
  66. package/components/TextArea.js +48 -16
  67. package/components/Title.js +8 -9
  68. package/components/Tooltip.js +51 -22
  69. package/components/TopAppBar.js +71 -78
  70. package/constants/shapes.js +36 -0
  71. package/constants/typography.js +127 -0
  72. package/core/Composition.js +391 -201
  73. package/core/CompositionAdapter.js +35 -18
  74. package/core/CustomElement.js +634 -254
  75. package/core/css.js +117 -12
  76. package/core/customTypes.js +161 -49
  77. package/core/dom.js +18 -11
  78. package/core/jsonMergePatch.js +27 -11
  79. package/core/observe.js +308 -256
  80. package/core/optimizations.js +9 -9
  81. package/core/template.js +14 -57
  82. package/dist/CustomElement.min.js +2 -0
  83. package/dist/CustomElement.min.js.map +7 -0
  84. package/dist/core/CustomElement.min.js +2 -0
  85. package/dist/core/CustomElement.min.js.map +7 -0
  86. package/dist/index.min.js +85 -115
  87. package/dist/index.min.js.map +4 -4
  88. package/dist/meta.json +1 -1
  89. package/dom/HTMLOptionsCollectionProxy.js +108 -0
  90. package/{theming/themableMixinLoader.js → loaders/palette.js} +4 -3
  91. package/loaders/theme.js +12 -0
  92. package/mixins/AriaReflectorMixin.js +64 -15
  93. package/mixins/AriaToolbarMixin.js +6 -0
  94. package/mixins/ControlMixin.js +79 -33
  95. package/mixins/DelegatesFocusMixin.js +62 -0
  96. package/mixins/DensityMixin.js +7 -3
  97. package/mixins/ElevationMixin.js +61 -0
  98. package/mixins/FlexableMixin.js +87 -39
  99. package/mixins/FormAssociatedMixin.js +76 -10
  100. package/mixins/HyperlinkMixin.js +76 -0
  101. package/mixins/InputMixin.js +227 -32
  102. package/mixins/KeyboardNavMixin.js +11 -7
  103. package/mixins/NavigationListenerMixin.js +33 -0
  104. package/mixins/PopupMixin.js +216 -219
  105. package/mixins/RTLObserverMixin.js +2 -0
  106. package/mixins/ResizeObserverMixin.js +18 -4
  107. package/mixins/RippleMixin.js +11 -7
  108. package/mixins/ScrollListenerMixin.js +14 -2
  109. package/mixins/SemiStickyMixin.js +51 -98
  110. package/mixins/ShapeMaskedMixin.js +125 -0
  111. package/mixins/ShapeMixin.js +30 -203
  112. package/mixins/StateMixin.js +74 -34
  113. package/mixins/TextFieldMixin.js +128 -145
  114. package/mixins/ThemableMixin.js +57 -56
  115. package/mixins/TooltipTriggerMixin.js +305 -359
  116. package/mixins/TouchTargetMixin.js +5 -2
  117. package/mixins/TypographyMixin.js +128 -0
  118. package/package.json +125 -81
  119. package/services/rtl.js +10 -0
  120. package/services/svgAlias.js +17 -0
  121. package/{theming/index.js → services/theme.js} +25 -176
  122. package/types/bin/mdw-css.d.ts +3 -0
  123. package/types/bin/mdw-css.d.ts.map +1 -0
  124. package/types/components/Badge.d.ts +39 -0
  125. package/types/components/Badge.d.ts.map +1 -0
  126. package/types/components/Body.d.ts +29 -0
  127. package/types/components/Body.d.ts.map +1 -0
  128. package/types/components/BottomAppBar.d.ts +72 -0
  129. package/types/components/BottomAppBar.d.ts.map +1 -0
  130. package/types/components/BottomSheet.d.ts +135 -0
  131. package/types/components/BottomSheet.d.ts.map +1 -0
  132. package/types/components/Box.d.ts +16 -0
  133. package/types/components/Box.d.ts.map +1 -0
  134. package/types/components/Button.d.ts +245 -0
  135. package/types/components/Button.d.ts.map +1 -0
  136. package/types/components/Card.d.ts +147 -0
  137. package/types/components/Card.d.ts.map +1 -0
  138. package/types/components/Checkbox.d.ts +207 -0
  139. package/types/components/Checkbox.d.ts.map +1 -0
  140. package/types/components/CheckboxIcon.d.ts +44 -0
  141. package/types/components/CheckboxIcon.d.ts.map +1 -0
  142. package/types/components/Chip.d.ts +248 -0
  143. package/types/components/Chip.d.ts.map +1 -0
  144. package/types/components/Dialog.d.ts +103 -0
  145. package/types/components/Dialog.d.ts.map +1 -0
  146. package/types/components/DialogActions.d.ts +4 -0
  147. package/types/components/DialogActions.d.ts.map +1 -0
  148. package/types/components/Display.d.ts +46 -0
  149. package/types/components/Display.d.ts.map +1 -0
  150. package/types/components/Divider.d.ts +10 -0
  151. package/types/components/Divider.d.ts.map +1 -0
  152. package/types/components/Fab.d.ts +273 -0
  153. package/types/components/Fab.d.ts.map +1 -0
  154. package/types/components/FabContainer.d.ts +10 -0
  155. package/types/components/FabContainer.d.ts.map +1 -0
  156. package/types/components/FilterChip.d.ts +256 -0
  157. package/types/components/FilterChip.d.ts.map +1 -0
  158. package/types/components/Grid.d.ts +38 -0
  159. package/types/components/Grid.d.ts.map +1 -0
  160. package/types/components/Headline.d.ts +46 -0
  161. package/types/components/Headline.d.ts.map +1 -0
  162. package/types/components/Icon.d.ts +55 -0
  163. package/types/components/Icon.d.ts.map +1 -0
  164. package/types/components/IconButton.d.ts +284 -0
  165. package/types/components/IconButton.d.ts.map +1 -0
  166. package/types/components/Input.d.ts +2528 -0
  167. package/types/components/Input.d.ts.map +1 -0
  168. package/types/components/InputChip.d.ts +85 -0
  169. package/types/components/InputChip.d.ts.map +1 -0
  170. package/types/components/Label.d.ts +29 -0
  171. package/types/components/Label.d.ts.map +1 -0
  172. package/types/components/List.d.ts +35 -0
  173. package/types/components/List.d.ts.map +1 -0
  174. package/types/components/ListItem.d.ts +124 -0
  175. package/types/components/ListItem.d.ts.map +1 -0
  176. package/types/components/ListOption.d.ts +158 -0
  177. package/types/components/ListOption.d.ts.map +1 -0
  178. package/types/components/Listbox.d.ts +763 -0
  179. package/types/components/Listbox.d.ts.map +1 -0
  180. package/types/components/Menu.d.ts +130 -0
  181. package/types/components/Menu.d.ts.map +1 -0
  182. package/types/components/MenuItem.d.ts +232 -0
  183. package/types/components/MenuItem.d.ts.map +1 -0
  184. package/types/components/NavBar.d.ts +20 -0
  185. package/types/components/NavBar.d.ts.map +1 -0
  186. package/types/components/NavBarItem.d.ts +97 -0
  187. package/types/components/NavBarItem.d.ts.map +1 -0
  188. package/types/components/NavDrawer.d.ts +107 -0
  189. package/types/components/NavDrawer.d.ts.map +1 -0
  190. package/types/components/NavDrawerItem.d.ts +97 -0
  191. package/types/components/NavDrawerItem.d.ts.map +1 -0
  192. package/types/components/NavItem.d.ts +99 -0
  193. package/types/components/NavItem.d.ts.map +1 -0
  194. package/types/components/NavRail.d.ts +108 -0
  195. package/types/components/NavRail.d.ts.map +1 -0
  196. package/types/components/NavRailItem.d.ts +97 -0
  197. package/types/components/NavRailItem.d.ts.map +1 -0
  198. package/types/components/Page.d.ts +25 -0
  199. package/types/components/Page.d.ts.map +1 -0
  200. package/types/components/Pane.d.ts +44 -0
  201. package/types/components/Pane.d.ts.map +1 -0
  202. package/types/components/Popup.d.ts +78 -0
  203. package/types/components/Popup.d.ts.map +1 -0
  204. package/types/components/Progress.d.ts +21 -0
  205. package/types/components/Progress.d.ts.map +1 -0
  206. package/types/components/Radio.d.ts +201 -0
  207. package/types/components/Radio.d.ts.map +1 -0
  208. package/types/components/RadioIcon.d.ts +46 -0
  209. package/types/components/RadioIcon.d.ts.map +1 -0
  210. package/types/components/Ripple.d.ts +35 -0
  211. package/types/components/Ripple.d.ts.map +1 -0
  212. package/types/components/Root.d.ts +68 -0
  213. package/types/components/Root.d.ts.map +1 -0
  214. package/types/components/Scrim.d.ts +6 -0
  215. package/types/components/Scrim.d.ts.map +1 -0
  216. package/types/components/Search.d.ts +516 -0
  217. package/types/components/Search.d.ts.map +1 -0
  218. package/types/components/SegmentedButton.d.ts +252 -0
  219. package/types/components/SegmentedButton.d.ts.map +1 -0
  220. package/types/components/SegmentedButtonGroup.d.ts +43 -0
  221. package/types/components/SegmentedButtonGroup.d.ts.map +1 -0
  222. package/types/components/Select.d.ts +158 -0
  223. package/types/components/Select.d.ts.map +1 -0
  224. package/types/components/Shape.d.ts +10 -0
  225. package/types/components/Shape.d.ts.map +1 -0
  226. package/types/components/SideSheet.d.ts +111 -0
  227. package/types/components/SideSheet.d.ts.map +1 -0
  228. package/types/components/Slider.d.ts +203 -0
  229. package/types/components/Slider.d.ts.map +1 -0
  230. package/types/components/Snackbar.d.ts +73 -0
  231. package/types/components/Snackbar.d.ts.map +1 -0
  232. package/types/components/SnackbarContainer.d.ts +6 -0
  233. package/types/components/SnackbarContainer.d.ts.map +1 -0
  234. package/types/components/Surface.d.ts +45 -0
  235. package/types/components/Surface.d.ts.map +1 -0
  236. package/types/components/Switch.d.ts +187 -0
  237. package/types/components/Switch.d.ts.map +1 -0
  238. package/types/components/SwitchIcon.d.ts +61 -0
  239. package/types/components/SwitchIcon.d.ts.map +1 -0
  240. package/types/components/Tab.d.ts +139 -0
  241. package/types/components/Tab.d.ts.map +1 -0
  242. package/types/components/TabContent.d.ts +124 -0
  243. package/types/components/TabContent.d.ts.map +1 -0
  244. package/types/components/TabList.d.ts +1111 -0
  245. package/types/components/TabList.d.ts.map +1 -0
  246. package/types/components/TabPanel.d.ts +28 -0
  247. package/types/components/TabPanel.d.ts.map +1 -0
  248. package/types/components/Table.d.ts +25 -0
  249. package/types/components/Table.d.ts.map +1 -0
  250. package/types/components/TextArea.d.ts +201 -0
  251. package/types/components/TextArea.d.ts.map +1 -0
  252. package/types/components/Title.d.ts +46 -0
  253. package/types/components/Title.d.ts.map +1 -0
  254. package/types/components/Tooltip.d.ts +49 -0
  255. package/types/components/Tooltip.d.ts.map +1 -0
  256. package/types/components/TopAppBar.d.ts +129 -0
  257. package/types/components/TopAppBar.d.ts.map +1 -0
  258. package/types/constants/colorKeywords.d.ts +2 -0
  259. package/types/constants/colorKeywords.d.ts.map +1 -0
  260. package/types/constants/shapes.d.ts +38 -0
  261. package/types/constants/shapes.d.ts.map +1 -0
  262. package/types/constants/typography.d.ts +212 -0
  263. package/types/constants/typography.d.ts.map +1 -0
  264. package/types/core/Composition.d.ts +260 -0
  265. package/types/core/Composition.d.ts.map +1 -0
  266. package/types/core/CompositionAdapter.d.ts +114 -0
  267. package/types/core/CompositionAdapter.d.ts.map +1 -0
  268. package/types/core/CustomElement.d.ts +304 -0
  269. package/types/core/CustomElement.d.ts.map +1 -0
  270. package/types/core/css.d.ts +44 -0
  271. package/types/core/css.d.ts.map +1 -0
  272. package/types/core/customTypes.d.ts +22 -0
  273. package/types/core/customTypes.d.ts.map +1 -0
  274. package/types/core/dom.d.ts +32 -0
  275. package/types/core/dom.d.ts.map +1 -0
  276. package/types/core/jsonMergePatch.d.ts +31 -0
  277. package/types/core/jsonMergePatch.d.ts.map +1 -0
  278. package/types/core/observe.d.ts +114 -0
  279. package/types/core/observe.d.ts.map +1 -0
  280. package/types/core/optimizations.d.ts +7 -0
  281. package/types/core/optimizations.d.ts.map +1 -0
  282. package/types/core/template.d.ts +47 -0
  283. package/types/core/template.d.ts.map +1 -0
  284. package/types/core/uid.d.ts +6 -0
  285. package/types/core/uid.d.ts.map +1 -0
  286. package/types/dom/HTMLOptionsCollectionProxy.d.ts +18 -0
  287. package/types/dom/HTMLOptionsCollectionProxy.d.ts.map +1 -0
  288. package/types/index.d.ts +88 -0
  289. package/types/index.d.ts.map +1 -0
  290. package/types/loaders/palette.d.ts +2 -0
  291. package/types/loaders/palette.d.ts.map +1 -0
  292. package/types/loaders/theme.d.ts +2 -0
  293. package/types/loaders/theme.d.ts.map +1 -0
  294. package/types/mixins/AriaReflectorMixin.d.ts +31 -0
  295. package/types/mixins/AriaReflectorMixin.d.ts.map +1 -0
  296. package/types/mixins/AriaToolbarMixin.d.ts +34 -0
  297. package/types/mixins/AriaToolbarMixin.d.ts.map +1 -0
  298. package/types/mixins/ControlMixin.d.ts +124 -0
  299. package/types/mixins/ControlMixin.d.ts.map +1 -0
  300. package/types/mixins/DelegatesFocusMixin.d.ts +13 -0
  301. package/types/mixins/DelegatesFocusMixin.d.ts.map +1 -0
  302. package/types/mixins/DensityMixin.d.ts +8 -0
  303. package/types/mixins/DensityMixin.d.ts.map +1 -0
  304. package/types/mixins/ElevationMixin.d.ts +32 -0
  305. package/types/mixins/ElevationMixin.d.ts.map +1 -0
  306. package/types/mixins/FlexableMixin.d.ts +14 -0
  307. package/types/mixins/FlexableMixin.d.ts.map +1 -0
  308. package/types/mixins/FormAssociatedMixin.d.ts +123 -0
  309. package/types/mixins/FormAssociatedMixin.d.ts.map +1 -0
  310. package/types/mixins/HyperlinkMixin.d.ts +25 -0
  311. package/types/mixins/HyperlinkMixin.d.ts.map +1 -0
  312. package/types/mixins/InputMixin.d.ts +173 -0
  313. package/types/mixins/InputMixin.d.ts.map +1 -0
  314. package/types/mixins/KeyboardNavMixin.d.ts +46 -0
  315. package/types/mixins/KeyboardNavMixin.d.ts.map +1 -0
  316. package/types/mixins/NavigationListenerMixin.d.ts +8 -0
  317. package/types/mixins/NavigationListenerMixin.d.ts.map +1 -0
  318. package/types/mixins/PopupMixin.d.ts +98 -0
  319. package/types/mixins/PopupMixin.d.ts.map +1 -0
  320. package/types/mixins/RTLObserverMixin.d.ts +8 -0
  321. package/types/mixins/RTLObserverMixin.d.ts.map +1 -0
  322. package/types/mixins/ResizeObserverMixin.d.ts +13 -0
  323. package/types/mixins/ResizeObserverMixin.d.ts.map +1 -0
  324. package/types/mixins/RippleMixin.d.ts +94 -0
  325. package/types/mixins/RippleMixin.d.ts.map +1 -0
  326. package/types/mixins/ScrollListenerMixin.d.ts +46 -0
  327. package/types/mixins/ScrollListenerMixin.d.ts.map +1 -0
  328. package/types/mixins/SemiStickyMixin.d.ts +50 -0
  329. package/types/mixins/SemiStickyMixin.d.ts.map +1 -0
  330. package/types/mixins/ShapeMaskedMixin.d.ts +12 -0
  331. package/types/mixins/ShapeMaskedMixin.d.ts.map +1 -0
  332. package/types/mixins/ShapeMixin.d.ts +39 -0
  333. package/types/mixins/ShapeMixin.d.ts.map +1 -0
  334. package/types/mixins/StateMixin.d.ts +29 -0
  335. package/types/mixins/StateMixin.d.ts.map +1 -0
  336. package/types/mixins/TextFieldMixin.d.ts +153 -0
  337. package/types/mixins/TextFieldMixin.d.ts.map +1 -0
  338. package/types/mixins/ThemableMixin.d.ts +10 -0
  339. package/types/mixins/ThemableMixin.d.ts.map +1 -0
  340. package/types/mixins/TooltipTriggerMixin.d.ts +114 -0
  341. package/types/mixins/TooltipTriggerMixin.d.ts.map +1 -0
  342. package/types/mixins/TouchTargetMixin.d.ts +6 -0
  343. package/types/mixins/TouchTargetMixin.d.ts.map +1 -0
  344. package/types/mixins/TypographyMixin.d.ts +20 -0
  345. package/types/mixins/TypographyMixin.d.ts.map +1 -0
  346. package/types/services/rtl.d.ts +3 -0
  347. package/types/services/rtl.d.ts.map +1 -0
  348. package/types/services/svgAlias.d.ts +13 -0
  349. package/types/services/svgAlias.d.ts.map +1 -0
  350. package/types/services/theme.d.ts +45 -0
  351. package/types/services/theme.d.ts.map +1 -0
  352. package/types/utils/cli.d.ts +3 -0
  353. package/types/utils/cli.d.ts.map +1 -0
  354. package/types/utils/function.d.ts +3 -0
  355. package/types/utils/function.d.ts.map +1 -0
  356. package/types/utils/jsx-runtime.d.ts +20 -0
  357. package/types/utils/jsx-runtime.d.ts.map +1 -0
  358. package/types/utils/material-color/blend.d.ts +34 -0
  359. package/types/utils/material-color/blend.d.ts.map +1 -0
  360. package/types/utils/material-color/hct/Cam16.d.ts +142 -0
  361. package/types/utils/material-color/hct/Cam16.d.ts.map +1 -0
  362. package/types/utils/material-color/hct/Hct.d.ts +93 -0
  363. package/types/utils/material-color/hct/Hct.d.ts.map +1 -0
  364. package/types/utils/material-color/hct/ViewingConditions.d.ts +69 -0
  365. package/types/utils/material-color/hct/ViewingConditions.d.ts.map +1 -0
  366. package/types/utils/material-color/hct/hctSolver.d.ts +30 -0
  367. package/types/utils/material-color/hct/hctSolver.d.ts.map +1 -0
  368. package/types/utils/material-color/helper.d.ts +8 -0
  369. package/types/utils/material-color/helper.d.ts.map +1 -0
  370. package/types/utils/material-color/palettes/CorePalette.d.ts +75 -0
  371. package/types/utils/material-color/palettes/CorePalette.d.ts.map +1 -0
  372. package/types/utils/material-color/palettes/TonalPalette.d.ts +38 -0
  373. package/types/utils/material-color/palettes/TonalPalette.d.ts.map +1 -0
  374. package/types/utils/material-color/scheme/Scheme.d.ts +264 -0
  375. package/types/utils/material-color/scheme/Scheme.d.ts.map +1 -0
  376. package/types/utils/material-color/utils/color.d.ts +172 -0
  377. package/types/utils/material-color/utils/color.d.ts.map +1 -0
  378. package/types/utils/material-color/utils/math.d.ts +94 -0
  379. package/types/utils/material-color/utils/math.d.ts.map +1 -0
  380. package/types/utils/pixelmatch.d.ts +22 -0
  381. package/types/utils/pixelmatch.d.ts.map +1 -0
  382. package/types/utils/popup.d.ts +106 -0
  383. package/types/utils/popup.d.ts.map +1 -0
  384. package/types/utils/searchParams.d.ts +3 -0
  385. package/types/utils/searchParams.d.ts.map +1 -0
  386. package/types/utils/svg.d.ts +7 -0
  387. package/types/utils/svg.d.ts.map +1 -0
  388. package/utils/jsx-runtime.js +9 -4
  389. package/utils/material-color/scheme/Scheme.js +1 -1
  390. package/utils/pixelmatch.js +363 -0
  391. package/utils/popup.js +86 -10
  392. package/utils/searchParams.js +22 -0
  393. package/components/Button.md +0 -61
  394. package/components/ExtendedFab.js +0 -32
  395. package/components/Layout.js +0 -504
  396. package/components/Nav.js +0 -38
  397. package/core/DomAdapter.js +0 -586
  398. package/core/ICustomElement.d.ts +0 -291
  399. package/core/ICustomElement.js +0 -1
  400. package/core/test.js +0 -126
  401. package/core/typings.d.ts +0 -142
  402. package/core/typings.js +0 -1
  403. package/mixins/SurfaceMixin.js +0 -127
  404. package/theming/loader.js +0 -22
  405. /package/{utils/color_keywords.js → constants/colorKeywords.js} +0 -0
@@ -1,4 +1,4 @@
1
- /* https://www.w3.org/WAI/ARIA/apg/patterns/combobox/ */
1
+ /* https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.combobox.iseditable?view=windowsdesktop-7.0#remarks */
2
2
 
3
3
  import CustomElement from '../core/CustomElement.js';
4
4
  import InputMixin from '../mixins/InputMixin.js';
@@ -11,6 +11,8 @@ import Popup from './Popup.js';
11
11
 
12
12
  /** @typedef {import('./Listbox.js').default} Listbox */
13
13
 
14
+ /** @typedef {import('../mixins/RippleMixin.js').default} RippleMixin */
15
+
14
16
  /** @type {InstanceType<import('./Popup.js').default>} */
15
17
  let sharedPopup;
16
18
 
@@ -22,10 +24,18 @@ function getSharedPopup() {
22
24
  sharedPopup.shapeStyle = 'extra-small';
23
25
  sharedPopup.color = 'surface';
24
26
  sharedPopup.matchSourceWidth = true;
27
+ sharedPopup.flow = 'corner';
28
+ sharedPopup.elevation = 2;
25
29
  }
26
30
  return sharedPopup;
27
31
  }
28
32
 
33
+ /**
34
+ * Text fields let users enter and edit text. This component implements
35
+ * autocomplete, suggestions, and listbox behaviors where applicable.
36
+ * @see https://m3.material.io/components/text-fields/specs
37
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
38
+ */
29
39
  export default CustomElement
30
40
  .extend()
31
41
  .mixin(ThemableMixin)
@@ -34,26 +44,102 @@ export default CustomElement
34
44
  .mixin(TextFieldMixin)
35
45
  .mixin(ResizeObserverMixin)
36
46
  .observe({
37
- suggestInline: 'boolean',
47
+ /**
48
+ * When true, the component inserts the best suggestion into the input field
49
+ * as inline text. The appended portion of the suggestion is selected so the
50
+ * user can accept it (e.g. press Enter) or continue typing to replace it.
51
+ * Typically combined with `autosuggestInline` to display candidates while
52
+ * typing.
53
+ * @see https://w3c.github.io/aria/#aria-autocomplete
54
+ */
55
+ autocompleteInline: 'boolean',
56
+
57
+ /**
58
+ * When set and not `'custom'`, `applyAutocompleteList()` filters the
59
+ * listbox options to items that start * with the typed text. This controls
60
+ * dropdown filtering but does not itself cause inline insertion.
61
+ * @see https://w3c.github.io/aria/#aria-autocomplete
62
+ */
63
+ autocompleteList: 'string',
64
+
65
+ /**
66
+ * When true, the component advances/searches suggestion candidates as the
67
+ * user types (calls `changeSuggestion({ startsWith })`). If
68
+ * `autosuggestInline` is true and `autocompleteInline` is also true the
69
+ * current candidate will be shown inline; if `autocompleteInline` is false
70
+ * only the candidate selection changes (no inline text insertion).
71
+ */
72
+ autosuggestInline: 'boolean',
73
+
74
+ /** If true, when listbox is open, arrow navigation will automatically select options. */
75
+ autoSelect: 'boolean',
76
+
77
+ /** True when the listbox popup is currently expanded. */
38
78
  _expanded: 'boolean',
79
+
80
+ /** If true, when listbox is open, <Esc> accepts current suggestion (same as Enter). */
81
+ acceptOnEscape: 'boolean',
82
+
39
83
  _listbox: {
40
84
  type: 'object',
41
- /** @type {Listbox} */
85
+ /** @type {InstanceType<Listbox>} */
42
86
  value: null,
43
87
  },
88
+
89
+ /** Currently focused suggestion value in the listbox. */
44
90
  _focusedValue: 'string',
45
- _focusedIndex: { value: -1 },
91
+
92
+ /** Focus position in set (1-based) for aria-posinset. */
46
93
  _focusedPosInSet: { value: -1 },
94
+
95
+ /** Computed size of the listbox (number of options). */
47
96
  _listboxSize: { value: -1 },
97
+
98
+ /** Draft input text while composing suggestions. */
48
99
  _draftInput: { type: 'string', nullable: false },
100
+
101
+ /** Whether a suggestion is currently available. */
102
+ _hasSuggestion: 'boolean',
103
+
104
+ /** The current selected/committed listbox value. */
105
+ _listboxValue: 'string',
106
+
107
+ /** Last computed listbox value (non-nullable string). */
108
+ _lastComputedListboxValue: { type: 'string', nullable: false },
109
+
110
+ _values: {
111
+ type: 'array',
112
+ /** @type {string[]} */
113
+ value: [],
114
+ },
115
+
116
+ /** Whether an input chip is currently selected in multi-select mode. */
117
+ _chipSelected: 'boolean',
49
118
  })
50
119
  .observe({
51
120
  _hasListbox({ _listbox }) {
52
- return _listbox ? 'listbox' : null;
121
+ return !!_listbox;
122
+ },
123
+ _isSelect({ type }) {
124
+ return type.toLowerCase() === 'select-one' || type.toLocaleLowerCase() === 'select-multiple';
125
+ },
126
+ })
127
+ .define({
128
+ listbox() {
129
+ return this._listbox;
53
130
  },
54
131
  })
55
132
  .set({
133
+ /** @type {EventListener} */
56
134
  _onListboxChangeListener: null,
135
+ /** @type {EventListener} */
136
+ _onListboxClickListener: null,
137
+ /** @type {EventListener} */
138
+ _onPopupFocusoutListener: null,
139
+ _suggestionText: '',
140
+ _suggestionValue: '',
141
+ /** @type {Pick<HTMLOptionElement, 'label'|'value'|'selected'>} */
142
+ _suggestionOption: null,
57
143
  })
58
144
  .define({
59
145
  stateTargetElement() { return this.refs.control; },
@@ -61,35 +147,95 @@ export default CustomElement
61
147
  .methods({
62
148
  onResizeObserved() {
63
149
  if (!this._expanded) return;
64
- const popup = getSharedPopup();
65
- popup.updatePopupPosition(this.refs.shape);
150
+ getSharedPopup().updatePopupPosition(this.refs.shape);
66
151
  },
67
152
  /**
153
+ * Listbox should close if clicking own selection
68
154
  * @param {Event} event
69
155
  */
70
- onListboxChange(event) {
71
- const selectedItem = this._listbox.selectedOptions.item(0);
72
- this.selectOption(selectedItem);
156
+ onListboxClick(event) {
73
157
  this.closeListbox();
74
158
  // Revert focus back
75
159
  this.refs.control.focus();
76
160
  },
161
+ /**
162
+ * @param {Event} event
163
+ */
164
+ onListboxChange(event) {
165
+ if (this.multiple) {
166
+ const values = [...this._listbox.selectedOptions].map((option) => option.value);
167
+ this._values = values;
168
+ if (this._input.value) {
169
+ this.closeListbox();
170
+ }
171
+ return;
172
+ }
173
+ const option = this._listbox.selectedOptions.item(0);
174
+ this.render({
175
+ selectedOption: option,
176
+ });
177
+
178
+ const { label: suggestionText, value: suggestionValue } = option;
179
+ this._suggestionText = suggestionText;
180
+ this._suggestionValue = suggestionValue;
181
+ this._suggestionOption = option;
182
+ this._hasSuggestion = true;
183
+ this.acceptSuggestion(true);
184
+ this.closeListbox();
185
+ this.refs.control.focus();
186
+ },
187
+ /** @param {FocusEvent} Event */
188
+ onPopupFocusout({ relatedTarget }) {
189
+ if (!this._expanded) return;
190
+ // Ignore if focus lost to pop-up (likely pointerdown).
191
+ if (relatedTarget) {
192
+ if (this === relatedTarget) return;
193
+ if (this.contains(/** @type {any} */ (relatedTarget))) return;
194
+ if (getSharedPopup().contains(/** @type {any} */ (relatedTarget))) return;
195
+ }
196
+ this.closeListbox();
197
+ },
198
+ applyAutocompleteList() {
199
+ const { _listbox, _draftInput } = this;
200
+ if (!_listbox) return;
201
+
202
+ const lowerCase = _draftInput.toLowerCase();
203
+ for (const option of _listbox) {
204
+ option.hidden = !option.label.toLowerCase().startsWith(lowerCase);
205
+ }
206
+ },
77
207
  showListbox() {
78
208
  // Move contents of list slot into top-layer
79
209
  // Should only have one element
80
210
 
81
- const { _listbox, refs } = this;
211
+ const _listbox = this._listbox;
82
212
  if (!_listbox) return;
83
213
  this._expanded = true;
84
- const { control, ariaListbox, shape } = refs;
85
- control.setAttribute('role', 'combobox');
214
+ const { ariaListbox, shape } = this.refs;
86
215
  ariaListbox.setAttribute('aria-hidden', 'false');
87
216
  const popup = getSharedPopup();
88
- document.body.append(popup);
217
+ if ('popover' in popup) {
218
+ this.insertAdjacentElement('afterend', popup);
219
+ popup.popover = 'manual';
220
+ popup.showPopover();
221
+ } else {
222
+ document.body.append(popup);
223
+ }
89
224
  popup.replaceChildren(_listbox);
90
- _listbox.selectedIndex = -1;
91
225
  popup.showPopup(shape, false);
226
+ popup.addEventListener('focusout', this._onPopupFocusoutListener);
227
+ if (!this._isSelect && !this.multiple) {
228
+ _listbox.value = this._listboxValue;
229
+ }
230
+ const [option] = _listbox.selectedOptions;
231
+ if (option) {
232
+ option.scrollIntoView({
233
+ behavior: 'instant',
234
+ block: 'nearest',
235
+ });
236
+ }
92
237
  },
238
+
93
239
  closeListbox() {
94
240
  this._expanded = false;
95
241
  const { _listbox } = this;
@@ -101,54 +247,272 @@ export default CustomElement
101
247
  // TODO: Animate
102
248
  popup.remove();
103
249
  },
104
-
250
+ toggleListbox() {
251
+ if (this._expanded) {
252
+ this.closeListbox();
253
+ } else {
254
+ this.showListbox();
255
+ }
256
+ },
105
257
  /**
106
- * @param {{label:string, value:string}} option
258
+ * @param {Pick<HTMLOptionElement, 'label'|'value'|'selected'>} option
107
259
  * @return {void}
108
260
  */
109
- selectOption(option) {
261
+ suggestOption(option) {
110
262
  this.render({
111
263
  selectedOption: option,
112
264
  });
113
265
 
114
- const { _draftInput, _value, _input } = this;
115
- const { label: suggestion, value } = option;
266
+ const {
267
+ _draftInput: currentInput,
268
+ _input: inputElement,
269
+ _isSelect,
270
+ autocompleteInline,
271
+ _expanded,
272
+ autoSelect,
273
+ } = this;
274
+ const { label: suggestionText, value: suggestionValue } = option;
275
+
276
+ this._suggestionText = suggestionText;
277
+ this._suggestionValue = suggestionValue;
278
+ this._suggestionOption = option;
279
+ this._hasSuggestion = true;
280
+ if (autoSelect) {
281
+ this.acceptSuggestion(true);
282
+ return;
283
+ }
116
284
 
117
- if (_draftInput && suggestion && suggestion.toLowerCase().startsWith(_draftInput.toLowerCase())) {
118
- _input.value = _draftInput + suggestion.slice(_draftInput.length);
119
- _input.setSelectionRange(_value.length, suggestion.length);
285
+ // Autocomplete Inline
286
+ if ((_isSelect && !this.multiple) || autocompleteInline) {
287
+ let valueText = suggestionText;
288
+ let selectionStart = 0;
289
+ if (_expanded) {
290
+ if (!_isSelect && suggestionText.toLowerCase().startsWith(currentInput.toLowerCase())) {
291
+ valueText = currentInput + suggestionText.slice(currentInput.length);
292
+ selectionStart = currentInput.length;
293
+ } else {
294
+ selectionStart = suggestionText.length;
295
+ }
296
+ } else {
297
+ selectionStart = 0;
298
+ }
299
+ inputElement.value = valueText; // Displayed value
300
+ if (autocompleteInline) {
301
+ inputElement.setSelectionRange(selectionStart, suggestionText.length);
302
+ }
303
+ if (!_expanded) {
304
+ this.acceptSuggestion(true);
305
+ }
306
+ }
307
+ },
308
+ acceptSuggestion(emitChange = false) {
309
+ if (!this._hasSuggestion) return;
310
+ if (this.readOnly) return;
311
+ const { _suggestionText, _suggestionValue, _input, multiple, _listbox, _values } = this;
312
+ if (multiple) {
313
+ const newArray = [..._values.filter(Boolean), _suggestionValue ?? _suggestionText];
314
+ this._values = [...new Set(newArray)];
120
315
  } else {
121
- _input.value = suggestion;
122
- _input.setSelectionRange(suggestion.length, suggestion.length);
316
+ this.value = _suggestionValue;
317
+ _input.value = _suggestionText;
318
+ this._draftInput = _suggestionText;
319
+ _listbox.value = _suggestionValue;
320
+ }
321
+ if (emitChange) {
322
+ this.dispatchEvent(new Event('change', { bubbles: true }));
123
323
  }
124
- this._value = value;
125
324
  },
126
- })
127
- .on({
128
- _focusedIndexChanged(previous, current) {
325
+ /**
326
+ * @param {Object} options
327
+ * @param {boolean} [options.first] first option
328
+ * @param {boolean} [options.last] first option
329
+ * @param {boolean} [options.next] next fosuable option
330
+ * @param {boolean} [options.previous] previous focusable option
331
+ * @param {string} [options.startsWith] option label starsWith
332
+ * @param {string} [options.value] matches option value
333
+ * @param {string} [options.label] matches option label
334
+ */
335
+ changeSuggestion({ first, last, next, previous, startsWith, value, label }) {
129
336
  const _listbox = this._listbox;
130
- const previousItem = _listbox.item(previous);
131
- if (previousItem) {
132
- previousItem.focused = false;
133
- }
134
- const currentItem = _listbox.item(current);
135
- if (currentItem) {
136
- this._focusedPosInSet = current + 1;
137
- currentItem.focused = true;
138
- this.selectOption(currentItem);
139
- } else {
337
+ let suggestion;
338
+ let suggestionIndex;
339
+ let current;
340
+ let currentIndex;
341
+ let backup;
342
+ let backupIndex = -1;
343
+ let optionIndex = -1;
344
+ for (const option of _listbox.options) {
345
+ optionIndex++;
346
+ if (!current && option.focused) {
347
+ current = option;
348
+ currentIndex = optionIndex;
349
+ if (suggestion && (!next && !last)) {
350
+ break;
351
+ }
352
+ }
353
+ if (option.hidden) continue;
354
+ if (last) {
355
+ suggestion = option;
356
+ suggestionIndex = optionIndex;
357
+ continue;
358
+ }
359
+ if (first) {
360
+ if (!suggestion) {
361
+ suggestion = option;
362
+ suggestionIndex = optionIndex;
363
+ }
364
+ continue;
365
+ }
366
+ if (startsWith != null) {
367
+ if (!suggestion && option.label.toLowerCase().startsWith(startsWith)) {
368
+ suggestion = option;
369
+ suggestionIndex = optionIndex;
370
+ }
371
+ continue;
372
+ }
373
+ if (value != null) {
374
+ if (!suggestion && option.value === value) {
375
+ suggestion = option;
376
+ suggestionIndex = optionIndex;
377
+ }
378
+ continue;
379
+ }
380
+ if (label != null) {
381
+ if (!suggestion && option.label === label) {
382
+ suggestion = option;
383
+ suggestionIndex = optionIndex;
384
+ }
385
+ continue;
386
+ }
387
+ if (currentIndex === optionIndex) continue;
388
+ if (previous) {
389
+ suggestion = option;
390
+ suggestionIndex = optionIndex;
391
+ } else if (next) {
392
+ if (current) {
393
+ suggestion = option;
394
+ suggestionIndex = optionIndex;
395
+ break;
396
+ } else if (!backup) {
397
+ backup = option;
398
+ backupIndex = optionIndex;
399
+ }
400
+ }
401
+ }
402
+ if (current && current !== suggestion) {
403
+ current.focused = false;
404
+ }
405
+ if (!suggestion) {
406
+ suggestionIndex = backupIndex;
407
+ suggestion = backup;
408
+ this._input.value = this._draftInput;
409
+ }
410
+ if (suggestionIndex === -1) {
140
411
  this._focusedPosInSet = -1;
412
+ this._hasSuggestion = false;
413
+ return;
414
+ }
415
+ this._focusedPosInSet = suggestionIndex + 1;
416
+ suggestion.focused = true;
417
+ suggestion.scrollIntoView({
418
+ behavior: 'instant',
419
+ block: 'nearest',
420
+ });
421
+ this.suggestOption(suggestion);
422
+ },
423
+ resetSuggestion() {
424
+ if (this._isSelect) {
425
+ this.changeSuggestion({ label: this._draftInput });
426
+ } else {
427
+ this.changeSuggestion({ value: this._listboxValue });
428
+ }
429
+ },
430
+ refreshMultiple() {
431
+ const { _values, multiple } = this;
432
+ if (!multiple) {
433
+ this.refs.chips.replaceChildren();
434
+ return;
435
+ }
436
+ let element = /** @type {InstanceType<import('./InputChip.js').default>} */ (this.refs.chips.firstElementChild);
437
+
438
+ for (let i = 0; i < _values.length; i++) {
439
+ const currentValue = _values[i];
440
+ let foundOption;
441
+ if (this.listbox) {
442
+ for (const option of this.listbox.options) {
443
+ if (option.value === currentValue) {
444
+ foundOption = option;
445
+ break;
446
+ }
447
+ }
448
+ }
449
+
450
+ element ??= /** @type {InstanceType<import('./InputChip.js').default>} */ (this.refs.chips.appendChild(document.createElement('mdw-input-chip')));
451
+ element.closeButton = true;
452
+ element.textContent = foundOption?.label || currentValue;
453
+ element.textContent = foundOption?.label || currentValue;
454
+ element.disabled = this.disabled;
455
+ element.readOnly = this.readOnly;
456
+ // eslint-disable-next-line unicorn/prefer-add-event-listener
457
+ element.onclose ??= this.onChipClose.bind(this);
458
+ element = /** @type {InstanceType<import('./InputChip.js').default>} */ (element.nextElementSibling);
459
+ }
460
+ while (element) {
461
+ const prev = element;
462
+ element = /** @type {InstanceType<import('./InputChip.js').default>} */ (element.nextElementSibling);
463
+ prev.remove();
464
+ }
465
+ this._chipSelected = false;
466
+ this._input.value = '';
467
+ this._draftInput = '';
468
+ this._listboxValue = '';
469
+ if (this.listbox) {
470
+ for (const option of this.listbox.options) {
471
+ option.selected = _values.includes(option.value);
472
+ }
473
+ }
474
+ },
475
+ populateInputFromListbox() {
476
+ if (this.multiple) {
477
+ this.refreshMultiple();
478
+ return;
479
+ }
480
+ if (!this._isSelect) return;
481
+ if (!this._listbox) return;
482
+
483
+ this._listbox.value = this._value;
484
+ const [option] = this._listbox.selectedOptions;
485
+ if (option) {
486
+ this._input.value = option.label;
487
+ this._draftInput = option.label;
141
488
  }
142
489
  },
490
+ /** @param {CloseEvent & {currentTarget: HTMLElement}} event */
491
+ onChipClose({ currentTarget }) {
492
+ /** @type {Node} */
493
+ let prev = currentTarget;
494
+ let elementIndex = 0;
495
+ while ((prev = prev.previousSibling)) {
496
+ elementIndex++;
497
+ }
498
+ currentTarget.remove();
499
+ this._values.splice(elementIndex, 1);
500
+ this._values = [...this._values];
501
+ },
143
502
  })
144
503
  .childEvents({
145
504
  control: {
505
+ click() {
506
+ if (!this._isSelect) return;
507
+ if (this.readOnly) return;
508
+ this.toggleListbox();
509
+ },
146
510
  input(event) {
147
- if (!this._listbox) return;
511
+ if (!this._hasListbox) return;
148
512
  // Intercept event and dispatch a new one.
149
513
  // This allow authors to modify listbox (filter) and value (custom pattern)
150
514
  event.stopPropagation();
151
- this.dispatchEvent(new InputEvent('input', {
515
+ const performDefault = this.dispatchEvent(new InputEvent('input', {
152
516
  composed: true,
153
517
  data: event.data,
154
518
  bubbles: true,
@@ -159,46 +523,124 @@ export default CustomElement
159
523
  targetRanges: event.getTargetRanges(),
160
524
  isComposing: event.isComposing,
161
525
  }));
162
- this._draftInput = event.currentTarget.value;
163
- this._focusedIndex = -1;
164
- if (!this._expanded && this._listbox.length) {
526
+ if (!performDefault) {
527
+ event.preventDefault();
528
+ return;
529
+ }
530
+ const value = /** @type {HTMLInputElement} */ (event.currentTarget).value;
531
+ this._draftInput = value;
532
+ if (this.autocompleteList != null && this.autocompleteList !== 'custom') {
533
+ this.applyAutocompleteList();
534
+ }
535
+ this.resetSuggestion();
536
+ if (value && !this._expanded && this._listbox.length) {
165
537
  this.showListbox();
166
538
  }
539
+ // Only inline suggest if appending characters
540
+ if (event.data != null && this.autosuggestInline) {
541
+ this.changeSuggestion({ startsWith: this._draftInput.toLocaleLowerCase() });
542
+ }
167
543
  },
168
544
  keydown(event) {
545
+ if (!this._listbox) return;
546
+ // Calls to return will not prevent default.
169
547
  switch (event.key) {
548
+ case 'Home':
549
+ if (!this._isSelect) return;
550
+ if (this.readOnly) return;
551
+ this.changeSuggestion({ first: true });
552
+ break;
553
+ case 'End':
554
+ this._chipSelected = false;
555
+ if (!this._isSelect) return;
556
+ if (this.readOnly) return;
557
+ this.changeSuggestion({ last: true });
558
+ break;
559
+ case 'ArrowDown':
560
+ case 'Down':
561
+ if (this.disabled) return;
562
+ if (this.readOnly) return;
563
+ this._chipSelected = false;
564
+ if (this.readOnly) return;
565
+ if (event.altKey) {
566
+ this.toggleListbox();
567
+ break;
568
+ }
569
+ if (!this._expanded && !this.autocompleteInline && !this._isSelect) return;
570
+ this.changeSuggestion({ next: true });
571
+ break;
170
572
  case 'ArrowUp':
171
573
  case 'Up':
574
+ if (this.disabled) return;
575
+ if (this.readOnly) return;
576
+ if (event.altKey) {
577
+ this.toggleListbox();
578
+ break;
579
+ }
580
+ this._chipSelected = false;
581
+ if (!this._expanded && !this.autocompleteInline && !this._isSelect) return;
582
+ this.changeSuggestion({ previous: true });
583
+ break;
584
+ case 'Escape':
172
585
  if (!this._expanded) return;
173
- if (this._focusedIndex <= 0) {
174
- this._focusedIndex = (this._listbox.length - 1);
586
+ event.stopImmediatePropagation();
587
+ event.preventDefault();
588
+ if (this.acceptOnEscape) {
589
+ this.acceptSuggestion(true);
175
590
  } else {
176
- this._focusedIndex--;
591
+ this.resetSuggestion();
177
592
  }
593
+ this.closeListbox();
178
594
  break;
179
- case 'ArrowDown':
180
- case 'Down':
595
+ case 'Space':
596
+ if (this.disabled) return;
597
+ if (this.readOnly) return;
598
+ if (!this._isSelect) return;
599
+ if (!this._listbox) return;
181
600
  if (this._expanded) {
182
- if (this._focusedIndex >= this._listbox.length - 1) {
183
- this._focusedIndex = 0;
184
- } else {
185
- this._focusedIndex++;
601
+ if (this.multiple && this._suggestionOption) {
602
+ this._suggestionOption.selected = !this._suggestionOption.selected;
603
+ this.closeListbox();
186
604
  }
187
605
  } else {
188
- if (!this._listbox) return;
189
606
  this.showListbox();
190
- this._focusedIndex = 0;
191
607
  }
192
608
  break;
193
- case 'Escape':
194
- if (!this._expanded) return;
195
- this.closeListbox();
196
- break;
609
+ case 'Backspace':
610
+ if (this.disabled) return;
611
+ if (this.readOnly) return;
612
+ if (!this.multiple) return;
613
+ if (this._isSelect) return;
614
+ if (!this._input.value) {
615
+ if (this._chipSelected) {
616
+ this._values.pop();
617
+ this._values = [...this._values];
618
+ } else if (this._values.length) {
619
+ this._chipSelected = true;
620
+ }
621
+ }
622
+ return;
197
623
  case 'Tab':
624
+ if (!this._expanded && this.multiple) return;
198
625
  this.closeListbox();
626
+ this.acceptSuggestion(true);
199
627
  event.stopPropagation();
200
- return; // Don't prevent default
628
+ return;
629
+ case 'Enter':
630
+ this._chipSelected = false;
631
+ if (!this._expanded) return;
632
+ event.stopImmediatePropagation();
633
+ event.preventDefault();
634
+ this.acceptSuggestion(true);
635
+ this.closeListbox();
636
+ break;
637
+ case ' ':
638
+ return;
201
639
  default:
640
+ if (this._isSelect && event.key.length === 1) {
641
+ this.changeSuggestion({ startsWith: event.key.toLocaleLowerCase() });
642
+ break;
643
+ }
202
644
  return;
203
645
  }
204
646
  event.stopPropagation(); // Avoid kbd within kbd (sub menus)
@@ -212,9 +654,8 @@ export default CustomElement
212
654
  */
213
655
  slotchange({ currentTarget }) {
214
656
  if (this._expanded) return;
215
- /** @type {Listbox[]} */
216
- const [listbox] = currentTarget.assignedElements();
217
- const { _listbox } = this;
657
+ const [listbox] = /** @type {InstanceType<Listbox>[]} */ (currentTarget.assignedElements());
658
+ const _listbox = this._listbox;
218
659
  if (_listbox === listbox) {
219
660
  // Internal already matches
220
661
  return;
@@ -222,58 +663,222 @@ export default CustomElement
222
663
  if (_listbox) {
223
664
  // Unbind and release
224
665
  _listbox.removeEventListener('change', this._onListboxChangeListener);
225
- this._onListboxChangeListener = null;
666
+ _listbox.removeEventListener('click', this._onListboxClickListener);
667
+ _listbox._handleFormReset = true;
226
668
  }
669
+ this._listbox = listbox;
227
670
  if (listbox) {
228
671
  // Bind and store
229
- this._onListboxChangeListener = this.onListboxChange.bind(this);
672
+ if (!this.multiple) {
673
+ listbox.required = true; // Don't allow unclick
674
+ }
675
+
676
+ listbox._handleFormReset = false;
230
677
  listbox.addEventListener('change', this._onListboxChangeListener);
678
+ listbox.addEventListener('click', this._onListboxChangeListener);
679
+ this.populateInputFromListbox();
231
680
  }
232
- this._listbox = listbox;
681
+ },
682
+ },
683
+ trailingIcon: {
684
+ '~click'() {
685
+ if (!this._listbox) return;
686
+ this.toggleListbox();
687
+ this.refs.control.focus();
233
688
  },
234
689
  },
235
690
  })
236
691
  .events({
237
692
  blur({ relatedTarget }) {
693
+ this._chipSelected = false;
238
694
  if (!this._expanded) return;
239
695
  // Ignore if focus lost to pop-up (likely pointerdown).
240
- if (relatedTarget && getSharedPopup().contains(relatedTarget)) return;
696
+ const popup = getSharedPopup();
697
+ if (popup === relatedTarget) return;
698
+ if (relatedTarget && popup.contains(/** @type {Node} */ (relatedTarget))) return;
699
+ if (popup.matches(':focus-within,:focus')) return;
241
700
  this.closeListbox();
242
701
  },
243
702
  })
244
703
  .expressions({
704
+ showTrailingIcon({ trailingIcon, _listbox, _expanded, readOnly }) {
705
+ if (trailingIcon != null) {
706
+ return trailingIcon;
707
+ }
708
+ if (_listbox && !readOnly) {
709
+ return _expanded ? 'arrow_drop_up' : 'arrow_drop_down';
710
+ }
711
+ return null;
712
+ },
713
+ computedTrailingIcon({ trailingIcon, _listbox, _expanded }) {
714
+ if (trailingIcon != null) {
715
+ return trailingIcon;
716
+ }
717
+ if (_listbox) {
718
+ return _expanded ? 'arrow_drop_up' : 'arrow_drop_down';
719
+ }
720
+ return null;
721
+ },
722
+ controlTypeAttrValue({ _isSelect, type }) {
723
+ if (_isSelect) return 'text';
724
+ return type;
725
+ },
726
+ controlReadonlyAttrValue({ _isSelect, type, readOnly }) {
727
+ if (_isSelect) return true;
728
+ return readOnly;
729
+ },
730
+ controlIsSelect({ _isSelect, type }) {
731
+ return _isSelect;
732
+ },
245
733
  ariaExpandedAttrValue({ _hasListbox, _expanded }) {
246
- return _hasListbox ? `${_expanded}` : null;
734
+ if (!_hasListbox) return null;
735
+ return `${_expanded}`;
247
736
  },
248
737
  ariaControlsAttrValue({ _hasListbox }) {
249
- return _hasListbox ? 'aria-listbox' : null;
738
+ if (!_hasListbox) return null;
739
+ return 'aria-listbox';
250
740
  },
251
- ariaAutocompleteAttrValue({ _hasListbox, suggestInline }) {
252
- return _hasListbox
253
- ? (suggestInline ? 'both' : 'list')
254
- : null;
741
+ /**
742
+ * Compute `aria-autocomplete` to describe the widget's behavior to AT.
743
+ *
744
+ * ARIA values:
745
+ * - `none` : no autocomplete behavior is provided
746
+ * - `inline`: the textbox shows an inline completion the user can accept
747
+ * - `list` : a popup listbox provides choices (no inline insertion)
748
+ * - `both` : both inline completion and a listbox are available
749
+ *
750
+ * Component mapping:
751
+ * - `autocompleteInline` -> inline completion behavior
752
+ * - `autocompleteList` (non-null) -> listbox filtering / dropdown present
753
+ * - `autosuggestInline` -> drives candidate selection as user types
754
+ *
755
+ * Note: the current implementation reports `'both'` when a named
756
+ * `autocompleteList` is present (even if `autocompleteInline` is false).
757
+ * A stricter mapping would return `list` when only a listbox is present
758
+ * and `both` only when both `autocompleteInline` and a listbox exist.
759
+ */
760
+ ariaAutocompleteAttrValue({ _hasListbox, autocompleteList, _isSelect, autocompleteInline }) {
761
+ if (!_hasListbox) return null;
762
+ if (_isSelect) return null;
763
+ if (autocompleteList != null) {
764
+ return 'both';
765
+ }
766
+ return 'inline';
255
767
  },
256
768
  ariaActiveDescendantAttrValue({ _hasListbox, _expanded, _focusedValue }) {
257
- return _hasListbox
258
- // eslint-disable-next-line unicorn/no-nested-ternary
259
- ? ((_expanded && _focusedValue) ? 'aria-active' : '')
260
- : null;
769
+ if (!_hasListbox) return null;
770
+ if (_expanded && _focusedValue) return 'aria-active';
771
+ return '';
772
+ },
773
+ controlRoleAttrValue({ _hasListbox }) {
774
+ if (_hasListbox) return 'combobox';
775
+ return null;
776
+ },
777
+ populatedState({ value, _badInput, _draftInput, type }) {
778
+ return !!value || _badInput || !!_draftInput || type === 'datetime-local';
779
+ },
780
+ })
781
+ .recompose(({ refs: { control, trailingIcon, shape, labelText } }) => {
782
+ // Can't cross DOM boundaries
783
+ control.setAttribute('aria-activedescendant', '{ariaActiveDescendantAttrValue}');
784
+ control.setAttribute('aria-autocomplete', '{ariaAutocompleteAttrValue}');
785
+ control.setAttribute('aria-controls', '{ariaControlsAttrValue}');
786
+ control.setAttribute('aria-expanded', '{ariaExpandedAttrValue}');
787
+ control.setAttribute('type', '{controlTypeAttrValue}');
788
+ control.setAttribute('role', '{controlRoleAttrValue}');
789
+ control.setAttribute('readonly', '{controlReadonlyAttrValue}');
790
+ control.setAttribute('autocomplete', 'off');
791
+ control.setAttribute('is-select', '{controlIsSelect}');
792
+ trailingIcon.setAttribute('mdw-if', '{showTrailingIcon}');
793
+ trailingIcon.setAttribute('icon', '{computedTrailingIcon}');
794
+ shape.setAttribute('trailing-icon', '{computedTrailingIcon}');
795
+ labelText.setAttribute('trailing-icon', '{computedTrailingIcon}');
796
+ })
797
+ .overrides({
798
+ _onSetValue(value) {
799
+ if (this.multiple) {
800
+ this._values = value.split(',').filter(Boolean);
801
+ } else if (this._isSelect) {
802
+ this._value = value;
803
+ } else {
804
+ // Apply user value to input and read back result to apply control to parse
805
+ this._input.value = value;
806
+ this._value = this._input.value;
807
+ }
808
+ },
809
+ _onControlValue(value) {
810
+ // Only accept values from accepted suggestions
811
+ if (this.multiple) {
812
+ if (value) {
813
+ this._chipSelected = false;
814
+ }
815
+ return;
816
+ }
817
+ this._value = value;
261
818
  },
262
819
  })
263
820
  .on({
264
- composed() {
265
- const { control } = this.refs;
266
- // Can't cross DOM boundaries
267
- control.setAttribute('aria-activedescendant', '{ariaActiveDescendantAttrValue}');
268
- control.setAttribute('aria-autocomplete', '{ariaAutocompleteAttrValue}');
269
- control.setAttribute('aria-controls', '{ariaControlsAttrValue}');
270
- control.setAttribute('aria-expanded', '{ariaExpandedAttrValue}');
821
+ _valueChanged(previous, current) {
822
+ if (this.multiple) return; // Handled by _values
823
+ if (this._isSelect) {
824
+ this.populateInputFromListbox();
825
+ } else {
826
+ this._listboxValue = current;
827
+ }
828
+ },
829
+ _valuesChanged(previous, current) {
830
+ if (this.multiple && current) {
831
+ this._value = current.join(',');
832
+ this.refreshMultiple();
833
+ }
834
+ },
835
+ _chipSelectedChanged(previous, current) {
836
+ if (!this.multiple) return;
837
+ const element = /** @type {HTMLOptionElement} */ (this.refs.chips.lastElementChild);
838
+ if (element) {
839
+ element.selected = current;
840
+ }
841
+ },
842
+ _listboxValueChanged(previous, current) {
843
+ if (!this._hasListbox) return;
844
+ this._listbox.value = current;
845
+ this._draftInput = current;
846
+ this.changeSuggestion({ value: current });
847
+ },
848
+ _expandedChanged(previous, current) {
849
+ this._useFormImplicitSubmission = !current;
850
+ },
851
+ constructed() {
852
+ this._onListboxChangeListener = this.onListboxChange.bind(this);
853
+ this._onListboxClickListener = this.onListboxClick.bind(this);
854
+ this._onPopupFocusoutListener = this.onPopupFocusout.bind(this);
855
+ document.addEventListener('DOMContentLoaded', () => this.populateInputFromListbox(), { once: true });
856
+ },
857
+ multipleChanged(previous, current) {
858
+ if (this.listbox) {
859
+ this.listbox.multiple = current;
860
+ }
861
+ if (current) {
862
+ this._onSetValue(this._input.value);
863
+ }
864
+ },
865
+ disabledStateChanged() {
866
+ this.refreshMultiple();
867
+ this._chipSelected = false;
868
+ this.closeListbox();
869
+ },
870
+ readOnlyChanged() {
871
+ this.refreshMultiple();
872
+ this._chipSelected = false;
873
+ this.closeListbox();
271
874
  },
272
875
  })
273
876
  .html`
877
+ <div id=chips mdw-if={multiple}></div>
274
878
  <slot id=slot></slot>
275
- <div id=aria-listbox role=listbox>
276
- <div id=aria-active role=option aria-setsize="{_listbox.length}" aria-posinset={_focusedPosInSet} aria-label={selectedOption.label}></div>
879
+ <div id=aria-listbox role=listbox mdw-if={_hasListbox}>
880
+ <div id=aria-active role=option aria-hidden=false aria-label={selectedOption.label}
881
+ aria-setsize="{_listbox.length}" aria-posinset={_focusedPosInSet}></div>
277
882
  </div>
278
883
  `
279
884
  .css`
@@ -284,5 +889,59 @@ export default CustomElement
284
889
  #aria-listbox {
285
890
  display: none;
286
891
  }
892
+
893
+ #trailing-icon {
894
+ align-self: center;
895
+ }
896
+
897
+ #control:where([type="button"], [is-select]) {
898
+ cursor: pointer;
899
+ }
900
+
901
+ #inline[multiple] {
902
+ gap: 8px;
903
+ }
904
+
905
+ mdw-input-chip {
906
+ align-self: baseline;
907
+
908
+ }
909
+
910
+ #control[multiple] {
911
+ align-self: baseline;
912
+ }
913
+
914
+ #chips {
915
+ display: contents;
916
+ }
917
+
918
+ #inline {
919
+ flex-wrap: wrap;
920
+ }
921
+
922
+ #inline:where([filled],[outlined]) {
923
+ padding-inline: 16px;
924
+ }
925
+
926
+ #control {
927
+ flex: 1 1 8ch;
928
+ }
929
+
930
+ #control[is-select][multiple] {
931
+ flex: 1 1 0px;
932
+ }
287
933
  `
934
+ .recompose(({ refs: { inline, chips } }) => {
935
+ inline.prepend(chips);
936
+ inline.setAttribute('multiple', '{multiple}');
937
+ })
938
+ .extend((Base) => class Input extends Base {
939
+ /** @type {InstanceType<ReturnType<RippleMixin>>['addRipple']} */
940
+ addRipple(...args) {
941
+ // @ts-ignore TODO: Fix invalid cast
942
+ if (!this.active) return null;
943
+ // @ts-ignore TODO: Fix invalid cast
944
+ return super.addRipple(...args);
945
+ }
946
+ })
288
947
  .autoRegister('mdw-input');