cx 26.0.11 → 26.0.13

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 (366) hide show
  1. package/build/charts/ScatterGraph.d.ts +4 -4
  2. package/build/charts/ScatterGraph.js +2 -2
  3. package/build/data/ArrayElementView.spec.d.ts +1 -0
  4. package/build/data/ArrayElementView.spec.js +81 -0
  5. package/build/data/Binding.spec.d.ts +1 -0
  6. package/build/data/Binding.spec.js +61 -0
  7. package/build/data/Expression.spec.d.ts +1 -0
  8. package/build/data/Expression.spec.js +196 -0
  9. package/build/data/Grouper.spec.d.ts +1 -0
  10. package/build/data/Grouper.spec.js +48 -0
  11. package/build/data/Ref.spec.d.ts +1 -0
  12. package/build/data/Ref.spec.js +72 -0
  13. package/build/data/Selector.d.ts +1 -1
  14. package/build/data/Store.spec.d.ts +1 -0
  15. package/build/data/Store.spec.js +19 -0
  16. package/build/data/StoreRef.spec.d.ts +1 -0
  17. package/build/data/StoreRef.spec.js +22 -0
  18. package/build/data/StringTemplate.spec.d.ts +1 -0
  19. package/build/data/StringTemplate.spec.js +112 -0
  20. package/build/data/StructuredSelector.spec.d.ts +1 -0
  21. package/build/data/StructuredSelector.spec.js +102 -0
  22. package/build/data/View.spec.d.ts +1 -0
  23. package/build/data/View.spec.js +44 -0
  24. package/build/data/ZoomIntoPropertyView.spec.d.ts +1 -0
  25. package/build/data/ZoomIntoPropertyView.spec.js +54 -0
  26. package/build/data/comparer.spec.d.ts +1 -0
  27. package/build/data/comparer.spec.js +50 -0
  28. package/build/data/computable.d.ts +3 -6
  29. package/build/data/computable.spec.d.ts +1 -0
  30. package/build/data/computable.spec.js +56 -0
  31. package/build/data/createAccessorModelProxy.d.ts +5 -3
  32. package/build/data/createAccessorModelProxy.spec.d.ts +1 -0
  33. package/build/data/createAccessorModelProxy.spec.js +30 -0
  34. package/build/data/createStructuredSelector.spec.d.ts +1 -0
  35. package/build/data/createStructuredSelector.spec.js +42 -0
  36. package/build/data/diff/diffs.spec.d.ts +1 -0
  37. package/build/data/diff/diffs.spec.js +45 -0
  38. package/build/data/getAccessor.spec.d.ts +1 -0
  39. package/build/data/getAccessor.spec.js +10 -0
  40. package/build/data/getSelector.spec.d.ts +1 -0
  41. package/build/data/getSelector.spec.js +36 -0
  42. package/build/data/ops/append.spec.d.ts +1 -0
  43. package/build/data/ops/append.spec.js +24 -0
  44. package/build/data/ops/filter.spec.d.ts +1 -0
  45. package/build/data/ops/filter.spec.js +25 -0
  46. package/build/data/ops/findTreeNode.d.ts +1 -1
  47. package/build/data/ops/findTreeNode.js +1 -1
  48. package/build/data/ops/findTreeNode.spec.d.ts +1 -0
  49. package/build/data/ops/findTreeNode.spec.js +20 -0
  50. package/build/data/ops/findTreePath.d.ts +1 -1
  51. package/build/data/ops/merge.spec.d.ts +1 -0
  52. package/build/data/ops/merge.spec.js +23 -0
  53. package/build/data/ops/removeTreeNodes.d.ts +1 -1
  54. package/build/data/ops/removeTreeNodes.js +1 -1
  55. package/build/data/ops/removeTreeNodes.spec.d.ts +1 -0
  56. package/build/data/ops/removeTreeNodes.spec.js +35 -0
  57. package/build/data/ops/updateArray.spec.d.ts +1 -0
  58. package/build/data/ops/updateArray.spec.js +33 -0
  59. package/build/data/ops/updateTree.d.ts +1 -1
  60. package/build/data/ops/updateTree.spec.d.ts +1 -0
  61. package/build/data/ops/updateTree.spec.js +44 -0
  62. package/build/hooks/invokeCallback.spec.d.ts +1 -0
  63. package/build/hooks/invokeCallback.spec.js +44 -0
  64. package/build/hooks/resolveCallback.spec.d.ts +1 -0
  65. package/build/hooks/resolveCallback.spec.js +35 -0
  66. package/build/hooks/store.spec.d.ts +1 -0
  67. package/build/hooks/store.spec.js +48 -0
  68. package/build/hooks/useTrigger.spec.d.ts +1 -0
  69. package/build/hooks/useTrigger.spec.js +59 -0
  70. package/build/jsx-runtime.d.ts +10 -10
  71. package/build/jsx-runtime.js +6 -0
  72. package/build/svg/util/Rect.d.ts +1 -1
  73. package/build/ui/ContentResolver.d.ts +16 -6
  74. package/build/ui/Controller.d.ts +7 -0
  75. package/build/ui/Controller.js +2 -1
  76. package/build/ui/Controller.spec.d.ts +1 -0
  77. package/build/ui/Controller.spec.js +247 -0
  78. package/build/ui/Cx.spec.d.ts +1 -0
  79. package/build/ui/Cx.spec.js +153 -0
  80. package/build/ui/DataProxy.spec.d.ts +1 -0
  81. package/build/ui/DataProxy.spec.js +208 -0
  82. package/build/ui/Instance.d.ts +1 -1
  83. package/build/ui/Instance.js +10 -10
  84. package/build/ui/IsolatedScope.spec.d.ts +1 -0
  85. package/build/ui/IsolatedScope.spec.js +42 -0
  86. package/build/ui/Prop.d.ts +12 -1
  87. package/build/ui/PureContainer.spec.d.ts +1 -0
  88. package/build/ui/PureContainer.spec.js +149 -0
  89. package/build/ui/Repeater.d.ts +3 -3
  90. package/build/ui/Repeater.spec.d.ts +1 -0
  91. package/build/ui/Repeater.spec.js +109 -0
  92. package/build/ui/Rescope.spec.d.ts +1 -0
  93. package/build/ui/Rescope.spec.js +134 -0
  94. package/build/ui/Restate.spec.d.ts +1 -0
  95. package/build/ui/Restate.spec.js +257 -0
  96. package/build/ui/Text.d.ts +14 -2
  97. package/build/ui/Text.js +3 -0
  98. package/build/ui/adapter/ArrayAdapter.js +4 -1
  99. package/build/ui/adapter/ArrayAdapter.spec.d.ts +1 -0
  100. package/build/ui/adapter/ArrayAdapter.spec.js +44 -0
  101. package/build/ui/adapter/TreeAdapter.spec.d.ts +1 -0
  102. package/build/ui/adapter/TreeAdapter.spec.js +71 -0
  103. package/build/ui/app/Url.spec.d.ts +1 -0
  104. package/build/ui/app/Url.spec.js +43 -0
  105. package/build/ui/app/startHotAppLoop.js +1 -1
  106. package/build/ui/createFunctionalComponent.d.ts +14 -1
  107. package/build/ui/createFunctionalComponent.js +7 -4
  108. package/build/ui/createFunctionalComponent.spec.d.ts +1 -0
  109. package/build/ui/createFunctionalComponent.spec.js +272 -0
  110. package/build/ui/expr.d.ts +3 -1
  111. package/build/ui/exprHelpers.d.ts +32 -0
  112. package/build/ui/exprHelpers.js +61 -0
  113. package/build/ui/index.d.ts +1 -0
  114. package/build/ui/index.js +1 -0
  115. package/build/ui/layout/ContentPlaceholder.spec.d.ts +1 -0
  116. package/build/ui/layout/ContentPlaceholder.spec.js +333 -0
  117. package/build/ui/layout/FirstVisibleChildLayout.spec.d.ts +1 -0
  118. package/build/ui/layout/FirstVisibleChildLayout.spec.js +101 -0
  119. package/build/ui/selection/KeySelection.d.ts +0 -8
  120. package/build/ui/selection/Selection.d.ts +9 -3
  121. package/build/util/Console.d.ts +1 -0
  122. package/build/util/Console.js +7 -3
  123. package/build/util/Format.spec.d.ts +1 -0
  124. package/build/util/Format.spec.js +58 -0
  125. package/build/util/TraversalStack.spec.d.ts +1 -0
  126. package/build/util/TraversalStack.spec.js +43 -0
  127. package/build/util/date/upperBoundCheck.spec.d.ts +1 -0
  128. package/build/util/date/upperBoundCheck.spec.js +22 -0
  129. package/build/util/getSearchQueryPredicate.spec.d.ts +1 -0
  130. package/build/util/getSearchQueryPredicate.spec.js +33 -0
  131. package/build/util/isValidIdentifierName.spec.d.ts +1 -0
  132. package/build/util/isValidIdentifierName.spec.js +28 -0
  133. package/build/util/routeAppend.spec.d.ts +1 -0
  134. package/build/util/routeAppend.spec.js +14 -0
  135. package/build/widgets/AccessorBindings.spec.d.ts +1 -0
  136. package/build/widgets/AccessorBindings.spec.js +40 -0
  137. package/build/widgets/Button.d.ts +3 -6
  138. package/build/widgets/Button.js +1 -1
  139. package/build/widgets/DocumentTitle.d.ts +2 -0
  140. package/build/widgets/Heading.d.ts +2 -2
  141. package/build/widgets/HtmlElement.d.ts +33 -8
  142. package/build/widgets/HtmlElement.js +7 -9
  143. package/build/widgets/HtmlElement.spec.d.ts +1 -0
  144. package/build/widgets/HtmlElement.spec.js +38 -0
  145. package/build/widgets/Icon.d.ts +3 -13
  146. package/build/widgets/List.d.ts +4 -0
  147. package/build/widgets/ReactElementWrapper.d.ts +29 -0
  148. package/build/widgets/ReactElementWrapper.js +59 -0
  149. package/build/widgets/drag-drop/DragSource.d.ts +3 -3
  150. package/build/widgets/drag-drop/DragSource.js +2 -3
  151. package/build/widgets/drag-drop/DropZone.d.ts +3 -3
  152. package/build/widgets/drag-drop/DropZone.js +2 -3
  153. package/build/widgets/form/Checkbox.d.ts +2 -0
  154. package/build/widgets/form/ColorField.d.ts +2 -0
  155. package/build/widgets/form/DateTimeField.d.ts +2 -0
  156. package/build/widgets/form/Field.d.ts +0 -2
  157. package/build/widgets/form/LabeledContainer.d.ts +9 -8
  158. package/build/widgets/form/LabeledContainer.js +9 -9
  159. package/build/widgets/form/LookupField.d.ts +57 -9
  160. package/build/widgets/form/MonthField.d.ts +2 -0
  161. package/build/widgets/form/NumberField.d.ts +2 -0
  162. package/build/widgets/form/Radio.d.ts +2 -0
  163. package/build/widgets/form/Select.d.ts +2 -0
  164. package/build/widgets/form/Slider.d.ts +3 -0
  165. package/build/widgets/form/Switch.d.ts +2 -0
  166. package/build/widgets/form/TextField.d.ts +34 -0
  167. package/build/widgets/form/TimeList.d.ts +16 -1
  168. package/build/widgets/form/TimeList.js +34 -62
  169. package/build/widgets/form/UploadButton.d.ts +34 -2
  170. package/build/widgets/form/UploadButton.js +3 -1
  171. package/build/widgets/form/ValidationGroup.spec.d.ts +1 -0
  172. package/build/widgets/form/ValidationGroup.spec.js +62 -0
  173. package/build/widgets/form/Validator.d.ts +33 -2
  174. package/build/widgets/form/Validator.js +3 -0
  175. package/build/widgets/grid/Grid.d.ts +9 -9
  176. package/build/widgets/grid/TreeNode.d.ts +6 -0
  177. package/build/widgets/index.d.ts +1 -0
  178. package/build/widgets/index.js +1 -0
  179. package/build/widgets/nav/MenuItem.d.ts +3 -2
  180. package/build/widgets/nav/Route.spec.d.ts +1 -0
  181. package/build/widgets/nav/Route.spec.js +15 -0
  182. package/build/widgets/nav/Scroller.d.ts +4 -6
  183. package/build/widgets/nav/Scroller.js +6 -3
  184. package/build/widgets/nav/Tab.d.ts +2 -2
  185. package/build/widgets/overlay/ContextMenu.d.ts +3 -3
  186. package/build/widgets/overlay/Overlay.d.ts +2 -1
  187. package/build/widgets/overlay/Overlay.js +1 -1
  188. package/build.js +133 -133
  189. package/dist/data.js +2 -2
  190. package/dist/jsx-runtime.js +6 -1
  191. package/dist/manifest.d.ts +1443 -0
  192. package/dist/manifest.js +852 -804
  193. package/dist/ui.js +91 -5
  194. package/dist/util.js +3 -0
  195. package/dist/widgets.js +520 -161
  196. package/package.json +46 -20
  197. package/src/charts/Chart.ts +108 -108
  198. package/src/charts/ScatterGraph.tsx +6 -6
  199. package/src/data/ArrayElementView.ts +90 -90
  200. package/src/data/AugmentedViewBase.ts +88 -88
  201. package/src/data/Binding.ts +104 -104
  202. package/src/data/ExposedRecordView.ts +95 -95
  203. package/src/data/ExposedValueView.ts +89 -89
  204. package/src/data/Expression.spec.ts +229 -229
  205. package/src/data/Expression.ts +233 -233
  206. package/src/data/Grouper.spec.ts +57 -57
  207. package/src/data/Grouper.ts +158 -158
  208. package/src/data/NestedDataView.ts +43 -43
  209. package/src/data/ReadOnlyDataView.ts +39 -39
  210. package/src/data/Ref.ts +104 -104
  211. package/src/data/Selector.ts +10 -10
  212. package/src/data/Store.ts +52 -52
  213. package/src/data/StoreProxy.ts +19 -19
  214. package/src/data/StoreRef.ts +66 -66
  215. package/src/data/StringTemplate.spec.ts +132 -132
  216. package/src/data/StringTemplate.ts +93 -93
  217. package/src/data/StructuredSelector.spec.ts +113 -113
  218. package/src/data/StructuredSelector.ts +146 -146
  219. package/src/data/SubscribableView.ts +63 -63
  220. package/src/data/ZoomIntoPropertyView.spec.ts +64 -64
  221. package/src/data/ZoomIntoPropertyView.ts +45 -45
  222. package/src/data/computable.spec.ts +87 -62
  223. package/src/data/computable.ts +3 -6
  224. package/src/data/createAccessorModelProxy.spec.tsx +102 -1
  225. package/src/data/createAccessorModelProxy.ts +9 -3
  226. package/src/data/createStructuredSelector.ts +62 -62
  227. package/src/data/getAccessor.spec.ts +11 -11
  228. package/src/data/getAccessor.ts +74 -74
  229. package/src/data/getSelector.spec.ts +43 -43
  230. package/src/data/getSelector.ts +66 -66
  231. package/src/data/ops/filter.spec.ts +35 -35
  232. package/src/data/ops/filter.ts +9 -9
  233. package/src/data/ops/findTreeNode.ts +1 -5
  234. package/src/data/ops/findTreePath.ts +1 -1
  235. package/src/data/ops/merge.ts +13 -13
  236. package/src/data/ops/removeTreeNodes.spec.ts +37 -37
  237. package/src/data/ops/removeTreeNodes.ts +2 -2
  238. package/src/data/ops/updateArray.spec.ts +69 -69
  239. package/src/data/ops/updateArray.ts +31 -31
  240. package/src/data/ops/updateTree.ts +1 -1
  241. package/src/data/test-types.ts +7 -7
  242. package/src/hooks/resolveCallback.spec.tsx +30 -7
  243. package/src/hooks/useTrigger.ts +26 -26
  244. package/src/index.scss +6 -6
  245. package/src/jsx-dev-runtime.ts +4 -4
  246. package/src/jsx-runtime.spec.tsx +402 -0
  247. package/src/jsx-runtime.ts +26 -22
  248. package/src/svg/BoundedObject.ts +101 -101
  249. package/src/svg/util/Rect.ts +105 -105
  250. package/src/ui/CSSHelper.ts +17 -17
  251. package/src/ui/ContentResolver.spec.tsx +172 -19
  252. package/src/ui/ContentResolver.ts +16 -8
  253. package/src/ui/Controller.ts +15 -2
  254. package/src/ui/Culture.ts +159 -159
  255. package/src/ui/DataProxy.ts +55 -55
  256. package/src/ui/FocusManager.ts +171 -171
  257. package/src/ui/Instance.ts +866 -868
  258. package/src/ui/Prop.ts +140 -112
  259. package/src/ui/RenderingContext.ts +99 -99
  260. package/src/ui/Repeater.ts +3 -12
  261. package/src/ui/Rescope.ts +49 -49
  262. package/src/ui/StructuredInstanceDataAccessor.ts +32 -32
  263. package/src/ui/Text.ts +21 -2
  264. package/src/ui/VDOM.ts +34 -34
  265. package/src/ui/adapter/ArrayAdapter.spec.ts +55 -55
  266. package/src/ui/adapter/ArrayAdapter.ts +4 -1
  267. package/src/ui/adapter/TreeAdapter.spec.ts +76 -76
  268. package/src/ui/adapter/TreeAdapter.ts +185 -185
  269. package/src/ui/app/History.ts +133 -133
  270. package/src/ui/app/Url.spec.ts +50 -50
  271. package/src/ui/app/startHotAppLoop.ts +41 -41
  272. package/src/ui/createFunctionalComponent.spec.tsx +53 -0
  273. package/src/ui/createFunctionalComponent.ts +86 -65
  274. package/src/ui/expr.ts +4 -1
  275. package/src/ui/exprHelpers.spec.ts +379 -0
  276. package/src/ui/exprHelpers.ts +78 -0
  277. package/src/ui/index.ts +47 -46
  278. package/src/ui/layout/Content.ts +30 -30
  279. package/src/ui/layout/FirstVisibleChildLayout.spec.tsx +1 -1
  280. package/src/ui/layout/FirstVisibleChildLayout.ts +60 -60
  281. package/src/ui/selection/KeySelection.ts +0 -12
  282. package/src/ui/selection/PropertySelection.ts +87 -87
  283. package/src/ui/selection/Selection.ts +13 -3
  284. package/src/util/Console.ts +13 -11
  285. package/src/util/Format.ts +267 -267
  286. package/src/util/addEventListenerWithOptions.ts +41 -41
  287. package/src/util/browserSupportsPassiveEventHandlers.ts +20 -20
  288. package/src/util/color/rgbToHsl.ts +35 -35
  289. package/src/util/getActiveElement.ts +4 -4
  290. package/src/util/hasKey.ts +18 -18
  291. package/src/util/index.ts +55 -55
  292. package/src/util/innerTextTrim.ts +10 -10
  293. package/src/util/isArray.ts +3 -3
  294. package/src/util/isDataRecord.ts +5 -5
  295. package/src/util/isDefined.ts +3 -3
  296. package/src/util/isString.ts +3 -3
  297. package/src/widgets/AccessorBindings.spec.tsx +26 -0
  298. package/src/widgets/Button.tsx +5 -17
  299. package/src/widgets/DocumentTitle.ts +95 -92
  300. package/src/widgets/Heading.ts +2 -2
  301. package/src/widgets/HtmlElement.spec.helpers.tsx +108 -0
  302. package/src/widgets/HtmlElement.spec.tsx +20 -12
  303. package/src/widgets/HtmlElement.tsx +82 -24
  304. package/src/widgets/Icon.ts +3 -17
  305. package/src/widgets/List.tsx +6 -0
  306. package/src/widgets/ReactElementWrapper.spec.tsx +452 -0
  307. package/src/widgets/ReactElementWrapper.tsx +108 -0
  308. package/src/widgets/Sandbox.ts +103 -103
  309. package/src/widgets/autoFocus.ts +9 -9
  310. package/src/widgets/cx.ts +63 -63
  311. package/src/widgets/drag-drop/DragSource.tsx +3 -4
  312. package/src/widgets/drag-drop/DropZone.tsx +3 -4
  313. package/src/widgets/form/Checkbox.tsx +3 -0
  314. package/src/widgets/form/ColorField.tsx +3 -0
  315. package/src/widgets/form/DateTimeField.tsx +5 -0
  316. package/src/widgets/form/Field.tsx +0 -3
  317. package/src/widgets/form/Label.tsx +1 -0
  318. package/src/widgets/form/LabeledContainer.ts +22 -26
  319. package/src/widgets/form/LookupField.spec.tsx +93 -0
  320. package/src/widgets/form/LookupField.tsx +104 -9
  321. package/src/widgets/form/MonthField.tsx +5 -0
  322. package/src/widgets/form/NumberField.tsx +3 -0
  323. package/src/widgets/form/Radio.tsx +5 -0
  324. package/src/widgets/form/Select.tsx +5 -0
  325. package/src/widgets/form/Slider.tsx +4 -0
  326. package/src/widgets/form/Switch.tsx +3 -0
  327. package/src/widgets/form/TextField.tsx +62 -0
  328. package/src/widgets/form/TimeList.tsx +84 -73
  329. package/src/widgets/form/UploadButton.tsx +53 -2
  330. package/src/widgets/form/Validator.ts +40 -3
  331. package/src/widgets/grid/Grid.tsx +9 -12
  332. package/src/widgets/grid/GridCell.ts +143 -143
  333. package/src/widgets/grid/TreeNode.tsx +9 -0
  334. package/src/widgets/icons/calendar.tsx +17 -17
  335. package/src/widgets/icons/check.tsx +13 -13
  336. package/src/widgets/icons/clear.tsx +15 -15
  337. package/src/widgets/icons/close.tsx +20 -20
  338. package/src/widgets/icons/cx.tsx +38 -38
  339. package/src/widgets/icons/drop-down.tsx +15 -15
  340. package/src/widgets/icons/file.tsx +13 -13
  341. package/src/widgets/icons/folder-open.tsx +15 -15
  342. package/src/widgets/icons/folder.tsx +13 -13
  343. package/src/widgets/icons/forward.tsx +22 -22
  344. package/src/widgets/icons/loading.tsx +24 -24
  345. package/src/widgets/icons/menu.tsx +17 -17
  346. package/src/widgets/icons/pixel-picker.tsx +18 -18
  347. package/src/widgets/icons/search.tsx +13 -13
  348. package/src/widgets/icons/sort-asc.tsx +14 -14
  349. package/src/widgets/icons/square.tsx +18 -18
  350. package/src/widgets/index.ts +1 -0
  351. package/src/widgets/nav/MenuItem.tsx +3 -2
  352. package/src/widgets/nav/Route.ts +142 -142
  353. package/src/widgets/nav/Scroller.tsx +8 -9
  354. package/src/widgets/nav/Tab.ts +2 -2
  355. package/src/widgets/overlay/ContextMenu.ts +42 -42
  356. package/src/widgets/overlay/Dropdown.tsx +762 -762
  357. package/src/widgets/overlay/MsgBox.tsx +141 -141
  358. package/src/widgets/overlay/Overlay.tsx +5 -4
  359. package/src/widgets/overlay/Toast.ts +111 -111
  360. package/src/widgets/overlay/Window.tsx +299 -299
  361. package/src/widgets/overlay/alerts.ts +46 -46
  362. package/src/widgets/overlay/captureMouse.ts +195 -195
  363. package/src/widgets/overlay/createHotPromiseWindowFactory.ts +72 -72
  364. package/src/widgets/overlay/index.d.ts +11 -11
  365. package/src/widgets/overlay/index.ts +11 -11
  366. package/src/widgets/overlay/tooltip-ops.ts +173 -173
@@ -1,762 +1,762 @@
1
- /** @jsxImportSource react */
2
- import { Localization } from "../../ui/Localization";
3
- import { ResizeManager } from "../../ui/ResizeManager";
4
- import { Widget, VDOM } from "../../ui/Widget";
5
- import { calculateNaturalElementHeight } from "../../util/calculateNaturalElementHeight";
6
- import { closestParent, findFirst, isFocusable } from "../../util/DOM";
7
- import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect";
8
- import { isTouchDevice } from "../../util/isTouchDevice";
9
- import { Overlay, OverlayBase, OverlayConfig, OverlayInstance } from "./Overlay";
10
- import { Instance } from "../../ui/Instance";
11
- import { StringProp } from "../../ui/Prop";
12
- import { RenderingContext } from "../../ui/RenderingContext";
13
- import { HtmlElement } from "../HtmlElement";
14
-
15
- /*
16
- Dropdown specific features:
17
- - ability to position itself next to the target element
18
- - monitor scrollable parents and updates it's position
19
- */
20
-
21
- export interface DropdownConfig extends OverlayConfig {
22
- /** Placement option for the dropdown relative to the trigger element. */
23
- placement?: StringProp | null;
24
-
25
- /** Offset distance from the trigger element. */
26
- offset?: number;
27
-
28
- /** Match the dropdown width to the trigger element. */
29
- matchWidth?: boolean;
30
-
31
- /** Match the dropdown max-width to the trigger element. */
32
- matchMaxWidth?: boolean;
33
-
34
- /** Placement preference order. */
35
- placementOrder?: string;
36
-
37
- /** Constrain the dropdown within the viewport. */
38
- constrain?: boolean;
39
-
40
- /** Positioning strategy - "fixed", "absolute", or "auto". */
41
- positioning?: string;
42
-
43
- /** Use touch-friendly positioning on touch devices. */
44
- touchFriendly?: boolean;
45
-
46
- /** Show an arrow pointing to the trigger element. */
47
- arrow?: boolean;
48
-
49
- /** Add padding around the dropdown. */
50
- pad?: boolean;
51
-
52
- /** Element explosion distance for positioning. */
53
- elementExplode?: number;
54
-
55
- /** Padding from screen edges. */
56
- screenPadding?: number;
57
-
58
- /** First child element defines the height. */
59
- firstChildDefinesHeight?: boolean;
60
-
61
- /** First child element defines the width. */
62
- firstChildDefinesWidth?: boolean;
63
-
64
- /** The dropdown will be automatically closed if the page is scrolled a certain distance. */
65
- closeOnScrollDistance?: number;
66
-
67
- /** The element to position the dropdown relative to. */
68
- relatedElement?: Element;
69
-
70
- /** Callback to resolve the related element. */
71
- onResolveRelatedElement?: string | ((beaconEl: Element, instance: any) => Element);
72
-
73
- /** Callback to measure natural content size. */
74
- onMeasureNaturalContentSize?: string | ((el: Element, instance: any) => { width?: number; height?: number });
75
-
76
- /** Callback when dropdown mounts. */
77
- onDropdownDidMount?: string;
78
-
79
- /** Callback to validate dropdown position. */
80
- pipeValidateDropdownPosition?: string;
81
-
82
- /** Callback when dropdown is dismissed after scroll. */
83
- onDismissAfterScroll?: string;
84
-
85
- /** Track mouse position for dropdowns. */
86
- trackMouse?: boolean;
87
-
88
- /** Track mouse X position. */
89
- trackMouseX?: boolean;
90
-
91
- /** Track mouse Y position. */
92
- trackMouseY?: boolean;
93
-
94
- /** Cover the related element with dropdown. */
95
- cover?: boolean;
96
- }
97
-
98
- export class DropdownInstance<
99
- WidgetType extends DropdownBase<any, any> = Dropdown,
100
- > extends OverlayInstance<WidgetType> {
101
- mousePosition?: any;
102
- parentPositionChangeEvent?: any;
103
- initialScreenPosition?: any;
104
- relatedElement?: HTMLElement;
105
- needsBeacon?: boolean;
106
- }
107
-
108
- export class DropdownBase<
109
- Config extends DropdownConfig = DropdownConfig,
110
- InstanceType extends DropdownInstance<any> = DropdownInstance<any>,
111
- > extends OverlayBase<Config, InstanceType> {
112
- declare trackMouse?: boolean;
113
- declare trackMouseX?: boolean;
114
- declare trackMouseY?: boolean;
115
- declare offset: number;
116
- declare matchWidth?: boolean;
117
- declare matchMaxWidth?: boolean;
118
- declare placementOrder: string;
119
- declare placement?: StringProp | null;
120
- declare constrain?: boolean;
121
- declare positioning?: string;
122
- declare touchFriendly?: boolean;
123
- declare arrow?: boolean;
124
- declare elementExplode?: number;
125
- declare screenPadding: number;
126
- declare firstChildDefinesHeight?: boolean;
127
- declare firstChildDefinesWidth?: boolean;
128
- declare closeOnScrollDistance: number;
129
- declare relatedElement?: HTMLElement;
130
- declare onResolveRelatedElement?: string | ((beaconEl: Element, instance: any) => Element);
131
- declare onMeasureNaturalContentSize?: string | ((el: Element, instance: any) => { width?: number; height?: number });
132
- declare onDropdownDidMount?: string;
133
- declare pipeValidateDropdownPosition?: string;
134
- declare onDismissAfterScroll?: string;
135
- declare onKeyDown?: string;
136
- declare cover?: boolean;
137
- declare mousePosition?: any;
138
- declare mouseTrap?: boolean;
139
- declare createDelay?: number;
140
-
141
- init() {
142
- if (this.trackMouse) {
143
- this.trackMouseX = true;
144
- this.trackMouseY = true;
145
- }
146
- if (this.autoFocus && !this.hasOwnProperty("focusable")) this.focusable = true;
147
- super.init();
148
- }
149
-
150
- declareData(...args: any[]) {
151
- return super.declareData(...args, {
152
- placement: undefined,
153
- });
154
- }
155
-
156
- initInstance(context: RenderingContext, instance: InstanceType): void {
157
- instance.mousePosition = this.mousePosition;
158
- instance.parentPositionChangeEvent = context.parentPositionChangeEvent;
159
- super.initInstance(context, instance);
160
- }
161
-
162
- explore(context: RenderingContext, instance: InstanceType): void {
163
- context.push("lastDropdown", instance);
164
- super.explore(context, instance);
165
- }
166
-
167
- exploreCleanup(context: RenderingContext, instance: InstanceType): void {
168
- context.pop("lastDropdown");
169
- super.exploreCleanup(context, instance);
170
- }
171
-
172
- overlayDidMount(instance: InstanceType, component: any): void {
173
- super.overlayDidMount(instance, component);
174
- var scrollableParents: Element[] = (component.scrollableParents = [window as any]);
175
- component.updateDropdownPosition = (e: any) => this.updateDropdownPosition(instance, component);
176
-
177
- instance.initialScreenPosition = null;
178
-
179
- var el = instance.relatedElement?.parentElement;
180
- while (el) {
181
- scrollableParents.push(el);
182
- el = el.parentElement;
183
- }
184
- scrollableParents.forEach((el: any) => {
185
- el.addEventListener("scroll", component.updateDropdownPosition);
186
- });
187
- component.offResize = ResizeManager.subscribe(component.updateDropdownPosition);
188
-
189
- if (this.onDropdownDidMount) instance.invoke("onDropdownDidMount", instance, component);
190
-
191
- if (this.pipeValidateDropdownPosition)
192
- instance.invoke("pipeValidateDropdownPosition", component.updateDropdownPosition, instance);
193
-
194
- if (instance.parentPositionChangeEvent)
195
- component.offParentPositionChange = instance.parentPositionChangeEvent.subscribe(
196
- component.updateDropdownPosition,
197
- );
198
- }
199
-
200
- overlayDidUpdate(instance: InstanceType, component: any): void {
201
- this.updateDropdownPosition(instance, component);
202
- }
203
-
204
- overlayWillUnmount(instance: InstanceType, component: any): void {
205
- var { scrollableParents } = component;
206
- if (scrollableParents) {
207
- scrollableParents.forEach((el: Element) => {
208
- el.removeEventListener("scroll", component.updateDropdownPosition);
209
- });
210
- delete component.scrollableParents;
211
- delete component.updateDropdownPosition;
212
- }
213
- if (component.offResize) component.offResize();
214
-
215
- if (this.pipeValidateDropdownPosition) instance.invoke("pipeValidateDropdownPosition", null, instance);
216
-
217
- if (component.offParentPositionChange) component.offParentPositionChange();
218
-
219
- delete component.parentBounds;
220
- delete component.initialScreenPosition;
221
- }
222
-
223
- dismissAfterScroll(data: any, instance: InstanceType, component: any): void {
224
- if (this.onDismissAfterScroll && instance.invoke("onDismissAfterScroll", data, instance, component) === false)
225
- return;
226
- if (instance.dismiss) instance.dismiss();
227
- }
228
-
229
- updateDropdownPosition(instance: InstanceType, component: any): void {
230
- var { el, initialScreenPosition } = component;
231
- var { data, relatedElement } = instance;
232
- var parentBounds = getTopLevelBoundingClientRect(relatedElement!);
233
-
234
- //getBoundingClientRect() will return an empty rect if the element is hidden or removed
235
- if (parentBounds.left == 0 && parentBounds.top == 0 && parentBounds.bottom == 0 && parentBounds.right == 0) {
236
- if (!component.parentBounds) return;
237
- parentBounds = component.parentBounds;
238
- } else component.parentBounds = parentBounds;
239
-
240
- if (this.trackMouseX && instance.mousePosition) {
241
- parentBounds = new DOMRect(
242
- instance.mousePosition.x,
243
- parentBounds.top,
244
- 0,
245
- parentBounds.bottom - parentBounds.top,
246
- );
247
- }
248
-
249
- if (this.trackMouseY && instance.mousePosition) {
250
- parentBounds = new DOMRect(
251
- parentBounds.left,
252
- instance.mousePosition.y,
253
- parentBounds.right - parentBounds.left,
254
- 0,
255
- );
256
- }
257
-
258
- let explode = this.pad && typeof this.elementExplode === "number" ? this.elementExplode : 0;
259
- if (explode) {
260
- parentBounds = new DOMRect(
261
- Math.round(parentBounds.left - explode),
262
- Math.round(parentBounds.top - explode),
263
- Math.round(parentBounds.right - parentBounds.left + 2 * explode),
264
- Math.round(parentBounds.bottom - parentBounds.top + 2 * explode),
265
- );
266
- }
267
-
268
- var style: any = {};
269
- if (this.matchWidth) style.minWidth = `${parentBounds.right - parentBounds.left}px`;
270
- if (this.matchMaxWidth) style.maxWidth = `${parentBounds.right - parentBounds.left}px`;
271
-
272
- var contentSize = this.measureNaturalDropdownSize(instance, component);
273
- var placement = this.findOptimalPlacement(contentSize, parentBounds, data.placement, component.lastPlacement);
274
-
275
- this.applyPositioningPlacementStyles(style, placement, contentSize, parentBounds, el, false);
276
- component.setCustomStyle(style);
277
- this.setDirectionClass(component, placement);
278
-
279
- if (this.constrain) {
280
- //recheck content size for changes as sometimes when auto is used the size can change
281
- let newContentSize = this.measureNaturalDropdownSize(instance, component);
282
- if (newContentSize.width != contentSize.width || newContentSize.height != contentSize.height) {
283
- let newStyle = {};
284
- this.applyPositioningPlacementStyles(newStyle, placement, newContentSize, parentBounds, el, true);
285
- component.setCustomStyle(newStyle);
286
- }
287
- }
288
-
289
- if (!initialScreenPosition) initialScreenPosition = component.initialScreenPosition = parentBounds;
290
-
291
- if (
292
- (!this.trackMouseY && Math.abs(parentBounds.top - initialScreenPosition.top) > this.closeOnScrollDistance) ||
293
- (!this.trackMouseX && Math.abs(parentBounds.left - initialScreenPosition.left) > this.closeOnScrollDistance)
294
- )
295
- this.dismissAfterScroll({ parentBounds, initialScreenPosition }, instance, component);
296
-
297
- instance.positionChangeSubscribers.notify();
298
- }
299
-
300
- applyFixedPositioningPlacementStyles(
301
- style: any,
302
- placement: string,
303
- contentSize: any,
304
- rel: any,
305
- el: HTMLElement,
306
- noAuto: boolean,
307
- ): void {
308
- let viewport = getViewportRect(this.screenPadding);
309
- style.position = "fixed";
310
-
311
- if (placement.startsWith("down")) {
312
- style.top = `${(this.cover ? rel.top : rel.bottom) + this.offset}px`;
313
- let bottom = viewport.bottom - (rel.bottom + this.offset + contentSize.height);
314
- style.bottom =
315
- this.constrain && (noAuto || bottom < this.screenPadding + 10)
316
- ? Math.max(this.screenPadding, bottom) + "px"
317
- : "auto";
318
- } else if (placement.startsWith("up")) {
319
- let top = rel.top - this.offset - contentSize.height - viewport.top;
320
- style.top =
321
- this.constrain && (noAuto || top < this.screenPadding + 10)
322
- ? Math.max(this.screenPadding, top) + "px"
323
- : "auto";
324
- style.bottom =
325
- document.documentElement.offsetHeight - (this.cover ? rel.bottom : rel.top) + this.offset + "px";
326
- }
327
-
328
- switch (placement) {
329
- case "down":
330
- case "down-center":
331
- style.right = "auto";
332
- style.left = `${Math.round((rel.left + rel.right - el.offsetWidth) / 2)}px`;
333
- break;
334
-
335
- case "down-right":
336
- style.right = "auto";
337
- style.left = `${rel.left}px`;
338
- break;
339
-
340
- case "down-left":
341
- style.right = `${document.documentElement.offsetWidth - rel.right}px`;
342
- style.left = "auto";
343
- break;
344
-
345
- case "up":
346
- case "up-center":
347
- style.right = "auto";
348
- style.left = `${Math.round((rel.left + rel.right - el.offsetWidth) / 2)}px`;
349
- break;
350
-
351
- case "up-right":
352
- style.right = "auto";
353
- style.left = `${rel.left}px`;
354
- break;
355
-
356
- case "up-left":
357
- style.right = `${document.documentElement.offsetWidth - rel.right}px`;
358
- style.left = "auto";
359
- break;
360
-
361
- case "right":
362
- case "right-center":
363
- style.top = `${Math.round((rel.top + rel.bottom - el.offsetHeight) / 2)}px`;
364
- style.right = "auto";
365
- style.bottom = "auto";
366
- style.left = `${rel.right + this.offset}px`;
367
- break;
368
-
369
- case "right-down":
370
- style.top = `${rel.top}px`;
371
- style.right = "auto";
372
- style.bottom = "auto";
373
- style.left = `${rel.right + this.offset}px`;
374
- break;
375
-
376
- case "right-up":
377
- style.top = "auto";
378
- style.right = "auto";
379
- style.bottom = `${document.documentElement.offsetHeight - rel.bottom}px`;
380
- style.left = `${rel.right + this.offset}px`;
381
- break;
382
-
383
- case "left":
384
- case "left-center":
385
- style.top = `${Math.round((rel.top + rel.bottom - el.offsetHeight) / 2)}px`;
386
- style.right = `${document.documentElement.offsetWidth - rel.left + this.offset}px`;
387
- style.bottom = "auto";
388
- style.left = "auto";
389
- break;
390
-
391
- case "left-down":
392
- style.top = `${rel.top}px`;
393
- style.right = `${document.documentElement.offsetWidth - rel.left + this.offset}px`;
394
- style.bottom = "auto";
395
- style.left = "auto";
396
- break;
397
-
398
- case "left-up":
399
- style.top = "auto";
400
- style.right = `${document.documentElement.offsetWidth - rel.left + this.offset}px`;
401
- style.bottom = `${document.documentElement.offsetHeight - rel.bottom}px`;
402
- style.left = "auto";
403
- break;
404
-
405
- case "screen-center":
406
- let w = Math.min(contentSize.width, document.documentElement.offsetWidth - 2 * this.screenPadding);
407
- let h = Math.min(contentSize.height, document.documentElement.offsetHeight - 2 * this.screenPadding);
408
- style.top = `${Math.round((document.documentElement.offsetHeight - h) / 2)}px`;
409
- style.right = `${Math.round((document.documentElement.offsetWidth - w) / 2)}px`;
410
- style.bottom = `${Math.round((document.documentElement.offsetHeight - h) / 2)}px`;
411
- style.left = `${Math.round((document.documentElement.offsetWidth - w) / 2)}px`;
412
- break;
413
- }
414
- }
415
-
416
- applyAbsolutePositioningPlacementStyles(
417
- style: any,
418
- placement: string,
419
- contentSize: any,
420
- rel: any,
421
- el: HTMLElement,
422
- noAuto: boolean,
423
- ): void {
424
- var viewport = getViewportRect(this.screenPadding);
425
-
426
- style.position = "absolute";
427
-
428
- if (placement.startsWith("down")) {
429
- style.top = `${rel.bottom - rel.top + this.offset}px`;
430
- let room = viewport.bottom - rel.bottom + this.offset;
431
- style.bottom =
432
- this.constrain && (noAuto || contentSize.height >= room - 10)
433
- ? `${-Math.min(room, contentSize.height)}px`
434
- : "auto";
435
- } else if (placement.startsWith("up")) {
436
- let room = rel.top - this.offset - viewport.top;
437
- style.top =
438
- this.constrain && (noAuto || contentSize.height >= room - 10)
439
- ? `${-Math.min(room, contentSize.height)}px`
440
- : "auto";
441
- style.bottom = `${rel.bottom - rel.top - this.offset}px`;
442
- }
443
-
444
- switch (placement) {
445
- case "down":
446
- case "down-center":
447
- style.right = "auto";
448
- style.left = `${Math.round((rel.right - rel.left - el.offsetWidth) / 2)}px`;
449
- break;
450
-
451
- case "down-right":
452
- style.right = "auto";
453
- style.left = `0`;
454
- break;
455
-
456
- case "down-left":
457
- style.right = `0`;
458
- style.left = "auto";
459
- break;
460
-
461
- case "up":
462
- case "up-center":
463
- style.right = "auto";
464
- style.left = `${Math.round((rel.right - rel.left - el.offsetWidth) / 2)}px`;
465
- break;
466
-
467
- case "up-right":
468
- style.right = "auto";
469
- style.left = `0`;
470
- break;
471
-
472
- case "up-left":
473
- style.right = `0`;
474
- style.left = "auto";
475
- break;
476
-
477
- case "right":
478
- case "right-center":
479
- style.top = `${Math.round((rel.bottom - rel.top - el.offsetHeight) / 2)}px`;
480
- style.right = "auto";
481
- style.bottom = "auto";
482
- style.left = `${rel.right - rel.left + this.offset}px`;
483
- break;
484
-
485
- case "right-down":
486
- style.top = `0`;
487
- style.right = "auto";
488
- style.bottom = "auto";
489
- style.left = `${rel.right - rel.left + this.offset}px`;
490
- break;
491
-
492
- case "right-up":
493
- style.top = "auto";
494
- style.right = "auto";
495
- style.bottom = `0`;
496
- style.left = `${rel.right - rel.left + this.offset}px`;
497
- break;
498
-
499
- case "left":
500
- case "left-center":
501
- style.top = `${Math.round((rel.bottom - rel.top - el.offsetHeight) / 2)}px`;
502
- style.right = `${rel.right - rel.left + this.offset}px`;
503
- style.bottom = "auto";
504
- style.left = "auto";
505
- break;
506
-
507
- case "left-down":
508
- style.top = `0`;
509
- style.right = `${rel.right - rel.left + this.offset}px`;
510
- style.bottom = "auto";
511
- style.left = "auto";
512
- break;
513
-
514
- case "left-up":
515
- style.top = "auto";
516
- style.right = `${rel.right - rel.left + this.offset}px`;
517
- style.bottom = `0`;
518
- style.left = "auto";
519
- break;
520
- }
521
- }
522
-
523
- applyPositioningPlacementStyles(
524
- style: any,
525
- placement: string,
526
- contentSize: any,
527
- parentBounds: any,
528
- el: HTMLElement,
529
- noAuto: boolean,
530
- ): void {
531
- switch (this.positioning) {
532
- case "absolute":
533
- this.applyAbsolutePositioningPlacementStyles(style, placement, contentSize, parentBounds, el, noAuto);
534
- break;
535
-
536
- case "auto":
537
- if (isTouchDevice())
538
- this.applyAbsolutePositioningPlacementStyles(style, placement, contentSize, parentBounds, el, noAuto);
539
- else this.applyFixedPositioningPlacementStyles(style, placement, contentSize, parentBounds, el, noAuto);
540
- break;
541
-
542
- default:
543
- this.applyFixedPositioningPlacementStyles(style, placement, contentSize, parentBounds, el, noAuto);
544
- break;
545
- }
546
- }
547
-
548
- setDirectionClass(component: any, placement: string): void {
549
- var state = {
550
- "place-left": false,
551
- "place-right": false,
552
- "place-up": false,
553
- "place-down": false,
554
- };
555
-
556
- component.lastPlacement = placement;
557
-
558
- component.setCSSState({
559
- ...state,
560
- ["place-" + placement]: true,
561
- });
562
- }
563
-
564
- measureNaturalDropdownSize(instance: InstanceType, component: any): any {
565
- var { el } = component;
566
- var size = {
567
- width: el.offsetWidth,
568
- height: this.constrain
569
- ? calculateNaturalElementHeight(el)
570
- : el.offsetHeight - el.clientHeight + el.scrollHeight,
571
- };
572
-
573
- if (this.firstChildDefinesHeight && el.firstChild) {
574
- size.height = el.firstChild.offsetHeight;
575
- }
576
-
577
- if (this.firstChildDefinesWidth && el.firstChild) {
578
- size.width = el.firstChild.offsetWidth;
579
- }
580
-
581
- if (this.onMeasureNaturalContentSize) {
582
- var more = instance.invoke("onMeasureNaturalContentSize", el, instance, component);
583
- Object.assign(size, more);
584
- }
585
-
586
- return size;
587
- }
588
-
589
- findOptimalPlacement(contentSize: any, target: any, placement: string, lastPlacement: any): any {
590
- var placementOrder = this.placementOrder.split(" ");
591
- var best = lastPlacement || placement;
592
- var first;
593
-
594
- var score: Record<string, number> = {};
595
- var viewport = getViewportRect();
596
-
597
- for (var i = 0; i < placementOrder.length; i++) {
598
- var p = placementOrder[i];
599
- if (!first) first = p;
600
- var parts = p.split("-");
601
-
602
- var primary = parts[0];
603
- var secondary = parts[1] || "center";
604
-
605
- score[p] = 0;
606
- var vertical = true;
607
-
608
- switch (primary) {
609
- case "down":
610
- score[p] += 3 * Math.min(1, (viewport.bottom - target.bottom - this.offset) / contentSize.height);
611
- break;
612
-
613
- case "up":
614
- score[p] += 3 * Math.min(1, (target.top - viewport.top - this.offset) / contentSize.height);
615
- break;
616
-
617
- case "right":
618
- score[p] += target.right + contentSize.width + this.offset < viewport.right ? 3 : 0;
619
- vertical = false;
620
- break;
621
-
622
- case "left":
623
- score[p] += target.left - contentSize.width - this.offset >= viewport.left ? 3 : 0;
624
- vertical = false;
625
- break;
626
- }
627
-
628
- switch (secondary) {
629
- case "center":
630
- if (vertical) {
631
- score[p] += (target.right + target.left - contentSize.width) / 2 >= viewport.left ? 1 : 0;
632
- score[p] += (target.right + target.left + contentSize.width) / 2 < viewport.right ? 1 : 0;
633
- } else {
634
- score[p] += (target.bottom + target.top - contentSize.height) / 2 >= viewport.top ? 1 : 0;
635
- score[p] += (target.bottom + target.top + contentSize.height) / 2 < viewport.bottom ? 1 : 0;
636
- }
637
- break;
638
-
639
- case "right":
640
- score[p] += target.left + contentSize.width < viewport.right ? 2 : 0;
641
- break;
642
-
643
- case "left":
644
- score[p] += target.right - contentSize.width >= viewport.left ? 2 : 0;
645
- break;
646
-
647
- case "up":
648
- score[p] += target.bottom - contentSize.height >= viewport.top ? 2 : 0;
649
- break;
650
-
651
- case "down":
652
- score[p] += target.top + contentSize.height < viewport.bottom ? 2 : 0;
653
- break;
654
- }
655
- }
656
-
657
- if (!(best in score)) best = first;
658
-
659
- for (var k in score) if (score[k] > score[best]) best = k;
660
-
661
- if (this.touchFriendly && isTouchDevice() && score[best] < 5) return "screen-center";
662
-
663
- return best;
664
- }
665
-
666
- handleKeyDown(e: React.KeyboardEvent, instance: InstanceType) {
667
- switch (e.keyCode) {
668
- case 27: //esc
669
- var focusable = findFirst(instance.relatedElement!, isFocusable);
670
- if (focusable) focusable.focus();
671
- e.stopPropagation();
672
- e.preventDefault();
673
- break;
674
- }
675
-
676
- if (this.onKeyDown) instance.invoke("onKeyDown", e, instance);
677
- }
678
-
679
- renderContents(context: RenderingContext, instance: InstanceType) {
680
- let { CSS, baseClass } = this;
681
- let result = [super.renderContents(context, instance)];
682
- if (this.arrow) {
683
- result.push(
684
- <div key="arrow-border" className={CSS.element(baseClass, "arrow-border")}></div>,
685
- <div key="arrow-back" className={CSS.element(baseClass, "arrow-fill")}></div>,
686
- );
687
- }
688
- return result;
689
- }
690
-
691
- render(context: RenderingContext, instance: InstanceType, key: string) {
692
- let { CSS, baseClass } = this;
693
- //if relatedElement is not provided, a beacon is rendered to and used to resolve a nearby element as a target
694
- //if onResolveTarget doesn't provide another element, the beacon itself is used as a target
695
- let beacon = null;
696
- if (this.relatedElement) instance.relatedElement = this.relatedElement;
697
-
698
- if (!this.relatedElement || instance.needsBeacon) {
699
- beacon = (
700
- <div
701
- key={`${key}-beacon`}
702
- className={CSS.element(baseClass, "beacon")}
703
- ref={(el) => {
704
- if (instance.relatedElement || !el) return;
705
- let target: HTMLElement | null = el;
706
- if (this.onResolveRelatedElement) target = instance.invoke("onResolveRelatedElement", el, instance);
707
- else target = el.previousElementSibling as HTMLElement;
708
- if (!target) target = el;
709
- if (target == el) instance.needsBeacon = true;
710
- instance.relatedElement = target;
711
- instance.setState({ dummy: {} });
712
- }}
713
- />
714
- );
715
- }
716
- return [beacon, instance.relatedElement && super.render(context, instance, key)];
717
- }
718
-
719
- getOverlayContainer(): HTMLElement {
720
- // this should be instance.relatedElement
721
- if (this.relatedElement) {
722
- let container = closestParent(
723
- this.relatedElement,
724
- (el) => el.dataset && !!el.dataset.focusableOverlayContainer,
725
- );
726
- if (container) return container;
727
- }
728
- return super.getOverlayContainer();
729
- }
730
- }
731
-
732
- DropdownBase.prototype.offset = 0;
733
- DropdownBase.prototype.baseClass = "dropdown";
734
- DropdownBase.prototype.matchWidth = true;
735
- DropdownBase.prototype.matchMaxWidth = false;
736
- DropdownBase.prototype.placementOrder = "up down right left";
737
- DropdownBase.prototype.placement = null; //default placement
738
- DropdownBase.prototype.constrain = false;
739
- DropdownBase.prototype.positioning = "fixed";
740
- DropdownBase.prototype.touchFriendly = false;
741
- DropdownBase.prototype.arrow = false;
742
- DropdownBase.prototype.pad = false;
743
- DropdownBase.prototype.elementExplode = 0;
744
- DropdownBase.prototype.closeOnScrollDistance = 50;
745
- DropdownBase.prototype.screenPadding = 5;
746
- DropdownBase.prototype.firstChildDefinesHeight = false;
747
- DropdownBase.prototype.firstChildDefinesWidth = false;
748
- DropdownBase.prototype.cover = false;
749
-
750
- export class Dropdown extends DropdownBase<DropdownConfig, DropdownInstance> {}
751
-
752
- Widget.alias("dropdown", Dropdown);
753
- Localization.registerPrototype("cx/widgets/Dropdown", Dropdown);
754
-
755
- function getViewportRect(padding = 0) {
756
- return {
757
- left: padding,
758
- top: padding,
759
- right: document.documentElement.offsetWidth - padding,
760
- bottom: document.documentElement.offsetHeight - padding,
761
- };
762
- }
1
+ /** @jsxImportSource react */
2
+ import { Localization } from "../../ui/Localization";
3
+ import { ResizeManager } from "../../ui/ResizeManager";
4
+ import { Widget, VDOM } from "../../ui/Widget";
5
+ import { calculateNaturalElementHeight } from "../../util/calculateNaturalElementHeight";
6
+ import { closestParent, findFirst, isFocusable } from "../../util/DOM";
7
+ import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect";
8
+ import { isTouchDevice } from "../../util/isTouchDevice";
9
+ import { Overlay, OverlayBase, OverlayConfig, OverlayInstance } from "./Overlay";
10
+ import { Instance } from "../../ui/Instance";
11
+ import { StringProp } from "../../ui/Prop";
12
+ import { RenderingContext } from "../../ui/RenderingContext";
13
+ import { HtmlElement } from "../HtmlElement";
14
+
15
+ /*
16
+ Dropdown specific features:
17
+ - ability to position itself next to the target element
18
+ - monitor scrollable parents and updates it's position
19
+ */
20
+
21
+ export interface DropdownConfig extends OverlayConfig {
22
+ /** Placement option for the dropdown relative to the trigger element. */
23
+ placement?: StringProp | null;
24
+
25
+ /** Offset distance from the trigger element. */
26
+ offset?: number;
27
+
28
+ /** Match the dropdown width to the trigger element. */
29
+ matchWidth?: boolean;
30
+
31
+ /** Match the dropdown max-width to the trigger element. */
32
+ matchMaxWidth?: boolean;
33
+
34
+ /** Placement preference order. */
35
+ placementOrder?: string;
36
+
37
+ /** Constrain the dropdown within the viewport. */
38
+ constrain?: boolean;
39
+
40
+ /** Positioning strategy - "fixed", "absolute", or "auto". */
41
+ positioning?: string;
42
+
43
+ /** Use touch-friendly positioning on touch devices. */
44
+ touchFriendly?: boolean;
45
+
46
+ /** Show an arrow pointing to the trigger element. */
47
+ arrow?: boolean;
48
+
49
+ /** Add padding around the dropdown. */
50
+ pad?: boolean;
51
+
52
+ /** Element explosion distance for positioning. */
53
+ elementExplode?: number;
54
+
55
+ /** Padding from screen edges. */
56
+ screenPadding?: number;
57
+
58
+ /** First child element defines the height. */
59
+ firstChildDefinesHeight?: boolean;
60
+
61
+ /** First child element defines the width. */
62
+ firstChildDefinesWidth?: boolean;
63
+
64
+ /** The dropdown will be automatically closed if the page is scrolled a certain distance. */
65
+ closeOnScrollDistance?: number;
66
+
67
+ /** The element to position the dropdown relative to. */
68
+ relatedElement?: Element;
69
+
70
+ /** Callback to resolve the related element. */
71
+ onResolveRelatedElement?: string | ((beaconEl: Element, instance: any) => Element);
72
+
73
+ /** Callback to measure natural content size. */
74
+ onMeasureNaturalContentSize?: string | ((el: Element, instance: any) => { width?: number; height?: number });
75
+
76
+ /** Callback when dropdown mounts. */
77
+ onDropdownDidMount?: string;
78
+
79
+ /** Callback to validate dropdown position. */
80
+ pipeValidateDropdownPosition?: string;
81
+
82
+ /** Callback when dropdown is dismissed after scroll. */
83
+ onDismissAfterScroll?: string;
84
+
85
+ /** Track mouse position for dropdowns. */
86
+ trackMouse?: boolean;
87
+
88
+ /** Track mouse X position. */
89
+ trackMouseX?: boolean;
90
+
91
+ /** Track mouse Y position. */
92
+ trackMouseY?: boolean;
93
+
94
+ /** Cover the related element with dropdown. */
95
+ cover?: boolean;
96
+ }
97
+
98
+ export class DropdownInstance<
99
+ WidgetType extends DropdownBase<any, any> = Dropdown,
100
+ > extends OverlayInstance<WidgetType> {
101
+ mousePosition?: any;
102
+ parentPositionChangeEvent?: any;
103
+ initialScreenPosition?: any;
104
+ relatedElement?: HTMLElement;
105
+ needsBeacon?: boolean;
106
+ }
107
+
108
+ export class DropdownBase<
109
+ Config extends DropdownConfig = DropdownConfig,
110
+ InstanceType extends DropdownInstance<any> = DropdownInstance<any>,
111
+ > extends OverlayBase<Config, InstanceType> {
112
+ declare trackMouse?: boolean;
113
+ declare trackMouseX?: boolean;
114
+ declare trackMouseY?: boolean;
115
+ declare offset: number;
116
+ declare matchWidth?: boolean;
117
+ declare matchMaxWidth?: boolean;
118
+ declare placementOrder: string;
119
+ declare placement?: StringProp | null;
120
+ declare constrain?: boolean;
121
+ declare positioning?: string;
122
+ declare touchFriendly?: boolean;
123
+ declare arrow?: boolean;
124
+ declare elementExplode?: number;
125
+ declare screenPadding: number;
126
+ declare firstChildDefinesHeight?: boolean;
127
+ declare firstChildDefinesWidth?: boolean;
128
+ declare closeOnScrollDistance: number;
129
+ declare relatedElement?: HTMLElement;
130
+ declare onResolveRelatedElement?: string | ((beaconEl: Element, instance: any) => Element);
131
+ declare onMeasureNaturalContentSize?: string | ((el: Element, instance: any) => { width?: number; height?: number });
132
+ declare onDropdownDidMount?: string;
133
+ declare pipeValidateDropdownPosition?: string;
134
+ declare onDismissAfterScroll?: string;
135
+ declare onKeyDown?: string;
136
+ declare cover?: boolean;
137
+ declare mousePosition?: any;
138
+ declare mouseTrap?: boolean;
139
+ declare createDelay?: number;
140
+
141
+ init() {
142
+ if (this.trackMouse) {
143
+ this.trackMouseX = true;
144
+ this.trackMouseY = true;
145
+ }
146
+ if (this.autoFocus && !this.hasOwnProperty("focusable")) this.focusable = true;
147
+ super.init();
148
+ }
149
+
150
+ declareData(...args: any[]) {
151
+ return super.declareData(...args, {
152
+ placement: undefined,
153
+ });
154
+ }
155
+
156
+ initInstance(context: RenderingContext, instance: InstanceType): void {
157
+ instance.mousePosition = this.mousePosition;
158
+ instance.parentPositionChangeEvent = context.parentPositionChangeEvent;
159
+ super.initInstance(context, instance);
160
+ }
161
+
162
+ explore(context: RenderingContext, instance: InstanceType): void {
163
+ context.push("lastDropdown", instance);
164
+ super.explore(context, instance);
165
+ }
166
+
167
+ exploreCleanup(context: RenderingContext, instance: InstanceType): void {
168
+ context.pop("lastDropdown");
169
+ super.exploreCleanup(context, instance);
170
+ }
171
+
172
+ overlayDidMount(instance: InstanceType, component: any): void {
173
+ super.overlayDidMount(instance, component);
174
+ var scrollableParents: Element[] = (component.scrollableParents = [window as any]);
175
+ component.updateDropdownPosition = (e: any) => this.updateDropdownPosition(instance, component);
176
+
177
+ instance.initialScreenPosition = null;
178
+
179
+ var el = instance.relatedElement?.parentElement;
180
+ while (el) {
181
+ scrollableParents.push(el);
182
+ el = el.parentElement;
183
+ }
184
+ scrollableParents.forEach((el: any) => {
185
+ el.addEventListener("scroll", component.updateDropdownPosition);
186
+ });
187
+ component.offResize = ResizeManager.subscribe(component.updateDropdownPosition);
188
+
189
+ if (this.onDropdownDidMount) instance.invoke("onDropdownDidMount", instance, component);
190
+
191
+ if (this.pipeValidateDropdownPosition)
192
+ instance.invoke("pipeValidateDropdownPosition", component.updateDropdownPosition, instance);
193
+
194
+ if (instance.parentPositionChangeEvent)
195
+ component.offParentPositionChange = instance.parentPositionChangeEvent.subscribe(
196
+ component.updateDropdownPosition,
197
+ );
198
+ }
199
+
200
+ overlayDidUpdate(instance: InstanceType, component: any): void {
201
+ this.updateDropdownPosition(instance, component);
202
+ }
203
+
204
+ overlayWillUnmount(instance: InstanceType, component: any): void {
205
+ var { scrollableParents } = component;
206
+ if (scrollableParents) {
207
+ scrollableParents.forEach((el: Element) => {
208
+ el.removeEventListener("scroll", component.updateDropdownPosition);
209
+ });
210
+ delete component.scrollableParents;
211
+ delete component.updateDropdownPosition;
212
+ }
213
+ if (component.offResize) component.offResize();
214
+
215
+ if (this.pipeValidateDropdownPosition) instance.invoke("pipeValidateDropdownPosition", null, instance);
216
+
217
+ if (component.offParentPositionChange) component.offParentPositionChange();
218
+
219
+ delete component.parentBounds;
220
+ delete component.initialScreenPosition;
221
+ }
222
+
223
+ dismissAfterScroll(data: any, instance: InstanceType, component: any): void {
224
+ if (this.onDismissAfterScroll && instance.invoke("onDismissAfterScroll", data, instance, component) === false)
225
+ return;
226
+ if (instance.dismiss) instance.dismiss();
227
+ }
228
+
229
+ updateDropdownPosition(instance: InstanceType, component: any): void {
230
+ var { el, initialScreenPosition } = component;
231
+ var { data, relatedElement } = instance;
232
+ var parentBounds = getTopLevelBoundingClientRect(relatedElement!);
233
+
234
+ //getBoundingClientRect() will return an empty rect if the element is hidden or removed
235
+ if (parentBounds.left == 0 && parentBounds.top == 0 && parentBounds.bottom == 0 && parentBounds.right == 0) {
236
+ if (!component.parentBounds) return;
237
+ parentBounds = component.parentBounds;
238
+ } else component.parentBounds = parentBounds;
239
+
240
+ if (this.trackMouseX && instance.mousePosition) {
241
+ parentBounds = new DOMRect(
242
+ instance.mousePosition.x,
243
+ parentBounds.top,
244
+ 0,
245
+ parentBounds.bottom - parentBounds.top,
246
+ );
247
+ }
248
+
249
+ if (this.trackMouseY && instance.mousePosition) {
250
+ parentBounds = new DOMRect(
251
+ parentBounds.left,
252
+ instance.mousePosition.y,
253
+ parentBounds.right - parentBounds.left,
254
+ 0,
255
+ );
256
+ }
257
+
258
+ let explode = this.pad && typeof this.elementExplode === "number" ? this.elementExplode : 0;
259
+ if (explode) {
260
+ parentBounds = new DOMRect(
261
+ Math.round(parentBounds.left - explode),
262
+ Math.round(parentBounds.top - explode),
263
+ Math.round(parentBounds.right - parentBounds.left + 2 * explode),
264
+ Math.round(parentBounds.bottom - parentBounds.top + 2 * explode),
265
+ );
266
+ }
267
+
268
+ var style: any = {};
269
+ if (this.matchWidth) style.minWidth = `${parentBounds.right - parentBounds.left}px`;
270
+ if (this.matchMaxWidth) style.maxWidth = `${parentBounds.right - parentBounds.left}px`;
271
+
272
+ var contentSize = this.measureNaturalDropdownSize(instance, component);
273
+ var placement = this.findOptimalPlacement(contentSize, parentBounds, data.placement, component.lastPlacement);
274
+
275
+ this.applyPositioningPlacementStyles(style, placement, contentSize, parentBounds, el, false);
276
+ component.setCustomStyle(style);
277
+ this.setDirectionClass(component, placement);
278
+
279
+ if (this.constrain) {
280
+ //recheck content size for changes as sometimes when auto is used the size can change
281
+ let newContentSize = this.measureNaturalDropdownSize(instance, component);
282
+ if (newContentSize.width != contentSize.width || newContentSize.height != contentSize.height) {
283
+ let newStyle = {};
284
+ this.applyPositioningPlacementStyles(newStyle, placement, newContentSize, parentBounds, el, true);
285
+ component.setCustomStyle(newStyle);
286
+ }
287
+ }
288
+
289
+ if (!initialScreenPosition) initialScreenPosition = component.initialScreenPosition = parentBounds;
290
+
291
+ if (
292
+ (!this.trackMouseY && Math.abs(parentBounds.top - initialScreenPosition.top) > this.closeOnScrollDistance) ||
293
+ (!this.trackMouseX && Math.abs(parentBounds.left - initialScreenPosition.left) > this.closeOnScrollDistance)
294
+ )
295
+ this.dismissAfterScroll({ parentBounds, initialScreenPosition }, instance, component);
296
+
297
+ instance.positionChangeSubscribers.notify();
298
+ }
299
+
300
+ applyFixedPositioningPlacementStyles(
301
+ style: any,
302
+ placement: string,
303
+ contentSize: any,
304
+ rel: any,
305
+ el: HTMLElement,
306
+ noAuto: boolean,
307
+ ): void {
308
+ let viewport = getViewportRect(this.screenPadding);
309
+ style.position = "fixed";
310
+
311
+ if (placement.startsWith("down")) {
312
+ style.top = `${(this.cover ? rel.top : rel.bottom) + this.offset}px`;
313
+ let bottom = viewport.bottom - (rel.bottom + this.offset + contentSize.height);
314
+ style.bottom =
315
+ this.constrain && (noAuto || bottom < this.screenPadding + 10)
316
+ ? Math.max(this.screenPadding, bottom) + "px"
317
+ : "auto";
318
+ } else if (placement.startsWith("up")) {
319
+ let top = rel.top - this.offset - contentSize.height - viewport.top;
320
+ style.top =
321
+ this.constrain && (noAuto || top < this.screenPadding + 10)
322
+ ? Math.max(this.screenPadding, top) + "px"
323
+ : "auto";
324
+ style.bottom =
325
+ document.documentElement.offsetHeight - (this.cover ? rel.bottom : rel.top) + this.offset + "px";
326
+ }
327
+
328
+ switch (placement) {
329
+ case "down":
330
+ case "down-center":
331
+ style.right = "auto";
332
+ style.left = `${Math.round((rel.left + rel.right - el.offsetWidth) / 2)}px`;
333
+ break;
334
+
335
+ case "down-right":
336
+ style.right = "auto";
337
+ style.left = `${rel.left}px`;
338
+ break;
339
+
340
+ case "down-left":
341
+ style.right = `${document.documentElement.offsetWidth - rel.right}px`;
342
+ style.left = "auto";
343
+ break;
344
+
345
+ case "up":
346
+ case "up-center":
347
+ style.right = "auto";
348
+ style.left = `${Math.round((rel.left + rel.right - el.offsetWidth) / 2)}px`;
349
+ break;
350
+
351
+ case "up-right":
352
+ style.right = "auto";
353
+ style.left = `${rel.left}px`;
354
+ break;
355
+
356
+ case "up-left":
357
+ style.right = `${document.documentElement.offsetWidth - rel.right}px`;
358
+ style.left = "auto";
359
+ break;
360
+
361
+ case "right":
362
+ case "right-center":
363
+ style.top = `${Math.round((rel.top + rel.bottom - el.offsetHeight) / 2)}px`;
364
+ style.right = "auto";
365
+ style.bottom = "auto";
366
+ style.left = `${rel.right + this.offset}px`;
367
+ break;
368
+
369
+ case "right-down":
370
+ style.top = `${rel.top}px`;
371
+ style.right = "auto";
372
+ style.bottom = "auto";
373
+ style.left = `${rel.right + this.offset}px`;
374
+ break;
375
+
376
+ case "right-up":
377
+ style.top = "auto";
378
+ style.right = "auto";
379
+ style.bottom = `${document.documentElement.offsetHeight - rel.bottom}px`;
380
+ style.left = `${rel.right + this.offset}px`;
381
+ break;
382
+
383
+ case "left":
384
+ case "left-center":
385
+ style.top = `${Math.round((rel.top + rel.bottom - el.offsetHeight) / 2)}px`;
386
+ style.right = `${document.documentElement.offsetWidth - rel.left + this.offset}px`;
387
+ style.bottom = "auto";
388
+ style.left = "auto";
389
+ break;
390
+
391
+ case "left-down":
392
+ style.top = `${rel.top}px`;
393
+ style.right = `${document.documentElement.offsetWidth - rel.left + this.offset}px`;
394
+ style.bottom = "auto";
395
+ style.left = "auto";
396
+ break;
397
+
398
+ case "left-up":
399
+ style.top = "auto";
400
+ style.right = `${document.documentElement.offsetWidth - rel.left + this.offset}px`;
401
+ style.bottom = `${document.documentElement.offsetHeight - rel.bottom}px`;
402
+ style.left = "auto";
403
+ break;
404
+
405
+ case "screen-center":
406
+ let w = Math.min(contentSize.width, document.documentElement.offsetWidth - 2 * this.screenPadding);
407
+ let h = Math.min(contentSize.height, document.documentElement.offsetHeight - 2 * this.screenPadding);
408
+ style.top = `${Math.round((document.documentElement.offsetHeight - h) / 2)}px`;
409
+ style.right = `${Math.round((document.documentElement.offsetWidth - w) / 2)}px`;
410
+ style.bottom = `${Math.round((document.documentElement.offsetHeight - h) / 2)}px`;
411
+ style.left = `${Math.round((document.documentElement.offsetWidth - w) / 2)}px`;
412
+ break;
413
+ }
414
+ }
415
+
416
+ applyAbsolutePositioningPlacementStyles(
417
+ style: any,
418
+ placement: string,
419
+ contentSize: any,
420
+ rel: any,
421
+ el: HTMLElement,
422
+ noAuto: boolean,
423
+ ): void {
424
+ var viewport = getViewportRect(this.screenPadding);
425
+
426
+ style.position = "absolute";
427
+
428
+ if (placement.startsWith("down")) {
429
+ style.top = `${rel.bottom - rel.top + this.offset}px`;
430
+ let room = viewport.bottom - rel.bottom + this.offset;
431
+ style.bottom =
432
+ this.constrain && (noAuto || contentSize.height >= room - 10)
433
+ ? `${-Math.min(room, contentSize.height)}px`
434
+ : "auto";
435
+ } else if (placement.startsWith("up")) {
436
+ let room = rel.top - this.offset - viewport.top;
437
+ style.top =
438
+ this.constrain && (noAuto || contentSize.height >= room - 10)
439
+ ? `${-Math.min(room, contentSize.height)}px`
440
+ : "auto";
441
+ style.bottom = `${rel.bottom - rel.top - this.offset}px`;
442
+ }
443
+
444
+ switch (placement) {
445
+ case "down":
446
+ case "down-center":
447
+ style.right = "auto";
448
+ style.left = `${Math.round((rel.right - rel.left - el.offsetWidth) / 2)}px`;
449
+ break;
450
+
451
+ case "down-right":
452
+ style.right = "auto";
453
+ style.left = `0`;
454
+ break;
455
+
456
+ case "down-left":
457
+ style.right = `0`;
458
+ style.left = "auto";
459
+ break;
460
+
461
+ case "up":
462
+ case "up-center":
463
+ style.right = "auto";
464
+ style.left = `${Math.round((rel.right - rel.left - el.offsetWidth) / 2)}px`;
465
+ break;
466
+
467
+ case "up-right":
468
+ style.right = "auto";
469
+ style.left = `0`;
470
+ break;
471
+
472
+ case "up-left":
473
+ style.right = `0`;
474
+ style.left = "auto";
475
+ break;
476
+
477
+ case "right":
478
+ case "right-center":
479
+ style.top = `${Math.round((rel.bottom - rel.top - el.offsetHeight) / 2)}px`;
480
+ style.right = "auto";
481
+ style.bottom = "auto";
482
+ style.left = `${rel.right - rel.left + this.offset}px`;
483
+ break;
484
+
485
+ case "right-down":
486
+ style.top = `0`;
487
+ style.right = "auto";
488
+ style.bottom = "auto";
489
+ style.left = `${rel.right - rel.left + this.offset}px`;
490
+ break;
491
+
492
+ case "right-up":
493
+ style.top = "auto";
494
+ style.right = "auto";
495
+ style.bottom = `0`;
496
+ style.left = `${rel.right - rel.left + this.offset}px`;
497
+ break;
498
+
499
+ case "left":
500
+ case "left-center":
501
+ style.top = `${Math.round((rel.bottom - rel.top - el.offsetHeight) / 2)}px`;
502
+ style.right = `${rel.right - rel.left + this.offset}px`;
503
+ style.bottom = "auto";
504
+ style.left = "auto";
505
+ break;
506
+
507
+ case "left-down":
508
+ style.top = `0`;
509
+ style.right = `${rel.right - rel.left + this.offset}px`;
510
+ style.bottom = "auto";
511
+ style.left = "auto";
512
+ break;
513
+
514
+ case "left-up":
515
+ style.top = "auto";
516
+ style.right = `${rel.right - rel.left + this.offset}px`;
517
+ style.bottom = `0`;
518
+ style.left = "auto";
519
+ break;
520
+ }
521
+ }
522
+
523
+ applyPositioningPlacementStyles(
524
+ style: any,
525
+ placement: string,
526
+ contentSize: any,
527
+ parentBounds: any,
528
+ el: HTMLElement,
529
+ noAuto: boolean,
530
+ ): void {
531
+ switch (this.positioning) {
532
+ case "absolute":
533
+ this.applyAbsolutePositioningPlacementStyles(style, placement, contentSize, parentBounds, el, noAuto);
534
+ break;
535
+
536
+ case "auto":
537
+ if (isTouchDevice())
538
+ this.applyAbsolutePositioningPlacementStyles(style, placement, contentSize, parentBounds, el, noAuto);
539
+ else this.applyFixedPositioningPlacementStyles(style, placement, contentSize, parentBounds, el, noAuto);
540
+ break;
541
+
542
+ default:
543
+ this.applyFixedPositioningPlacementStyles(style, placement, contentSize, parentBounds, el, noAuto);
544
+ break;
545
+ }
546
+ }
547
+
548
+ setDirectionClass(component: any, placement: string): void {
549
+ var state = {
550
+ "place-left": false,
551
+ "place-right": false,
552
+ "place-up": false,
553
+ "place-down": false,
554
+ };
555
+
556
+ component.lastPlacement = placement;
557
+
558
+ component.setCSSState({
559
+ ...state,
560
+ ["place-" + placement]: true,
561
+ });
562
+ }
563
+
564
+ measureNaturalDropdownSize(instance: InstanceType, component: any): any {
565
+ var { el } = component;
566
+ var size = {
567
+ width: el.offsetWidth,
568
+ height: this.constrain
569
+ ? calculateNaturalElementHeight(el)
570
+ : el.offsetHeight - el.clientHeight + el.scrollHeight,
571
+ };
572
+
573
+ if (this.firstChildDefinesHeight && el.firstChild) {
574
+ size.height = el.firstChild.offsetHeight;
575
+ }
576
+
577
+ if (this.firstChildDefinesWidth && el.firstChild) {
578
+ size.width = el.firstChild.offsetWidth;
579
+ }
580
+
581
+ if (this.onMeasureNaturalContentSize) {
582
+ var more = instance.invoke("onMeasureNaturalContentSize", el, instance, component);
583
+ Object.assign(size, more);
584
+ }
585
+
586
+ return size;
587
+ }
588
+
589
+ findOptimalPlacement(contentSize: any, target: any, placement: string, lastPlacement: any): any {
590
+ var placementOrder = this.placementOrder.split(" ");
591
+ var best = lastPlacement || placement;
592
+ var first;
593
+
594
+ var score: Record<string, number> = {};
595
+ var viewport = getViewportRect();
596
+
597
+ for (var i = 0; i < placementOrder.length; i++) {
598
+ var p = placementOrder[i];
599
+ if (!first) first = p;
600
+ var parts = p.split("-");
601
+
602
+ var primary = parts[0];
603
+ var secondary = parts[1] || "center";
604
+
605
+ score[p] = 0;
606
+ var vertical = true;
607
+
608
+ switch (primary) {
609
+ case "down":
610
+ score[p] += 3 * Math.min(1, (viewport.bottom - target.bottom - this.offset) / contentSize.height);
611
+ break;
612
+
613
+ case "up":
614
+ score[p] += 3 * Math.min(1, (target.top - viewport.top - this.offset) / contentSize.height);
615
+ break;
616
+
617
+ case "right":
618
+ score[p] += target.right + contentSize.width + this.offset < viewport.right ? 3 : 0;
619
+ vertical = false;
620
+ break;
621
+
622
+ case "left":
623
+ score[p] += target.left - contentSize.width - this.offset >= viewport.left ? 3 : 0;
624
+ vertical = false;
625
+ break;
626
+ }
627
+
628
+ switch (secondary) {
629
+ case "center":
630
+ if (vertical) {
631
+ score[p] += (target.right + target.left - contentSize.width) / 2 >= viewport.left ? 1 : 0;
632
+ score[p] += (target.right + target.left + contentSize.width) / 2 < viewport.right ? 1 : 0;
633
+ } else {
634
+ score[p] += (target.bottom + target.top - contentSize.height) / 2 >= viewport.top ? 1 : 0;
635
+ score[p] += (target.bottom + target.top + contentSize.height) / 2 < viewport.bottom ? 1 : 0;
636
+ }
637
+ break;
638
+
639
+ case "right":
640
+ score[p] += target.left + contentSize.width < viewport.right ? 2 : 0;
641
+ break;
642
+
643
+ case "left":
644
+ score[p] += target.right - contentSize.width >= viewport.left ? 2 : 0;
645
+ break;
646
+
647
+ case "up":
648
+ score[p] += target.bottom - contentSize.height >= viewport.top ? 2 : 0;
649
+ break;
650
+
651
+ case "down":
652
+ score[p] += target.top + contentSize.height < viewport.bottom ? 2 : 0;
653
+ break;
654
+ }
655
+ }
656
+
657
+ if (!(best in score)) best = first;
658
+
659
+ for (var k in score) if (score[k] > score[best]) best = k;
660
+
661
+ if (this.touchFriendly && isTouchDevice() && score[best] < 5) return "screen-center";
662
+
663
+ return best;
664
+ }
665
+
666
+ handleKeyDown(e: React.KeyboardEvent, instance: InstanceType) {
667
+ switch (e.keyCode) {
668
+ case 27: //esc
669
+ var focusable = findFirst(instance.relatedElement!, isFocusable);
670
+ if (focusable) focusable.focus();
671
+ e.stopPropagation();
672
+ e.preventDefault();
673
+ break;
674
+ }
675
+
676
+ if (this.onKeyDown) instance.invoke("onKeyDown", e, instance);
677
+ }
678
+
679
+ renderContents(context: RenderingContext, instance: InstanceType) {
680
+ let { CSS, baseClass } = this;
681
+ let result = [super.renderContents(context, instance)];
682
+ if (this.arrow) {
683
+ result.push(
684
+ <div key="arrow-border" className={CSS.element(baseClass, "arrow-border")}></div>,
685
+ <div key="arrow-back" className={CSS.element(baseClass, "arrow-fill")}></div>,
686
+ );
687
+ }
688
+ return result;
689
+ }
690
+
691
+ render(context: RenderingContext, instance: InstanceType, key: string) {
692
+ let { CSS, baseClass } = this;
693
+ //if relatedElement is not provided, a beacon is rendered to and used to resolve a nearby element as a target
694
+ //if onResolveTarget doesn't provide another element, the beacon itself is used as a target
695
+ let beacon = null;
696
+ if (this.relatedElement) instance.relatedElement = this.relatedElement;
697
+
698
+ if (!this.relatedElement || instance.needsBeacon) {
699
+ beacon = (
700
+ <div
701
+ key={`${key}-beacon`}
702
+ className={CSS.element(baseClass, "beacon")}
703
+ ref={(el) => {
704
+ if (instance.relatedElement || !el) return;
705
+ let target: HTMLElement | null = el;
706
+ if (this.onResolveRelatedElement) target = instance.invoke("onResolveRelatedElement", el, instance);
707
+ else target = el.previousElementSibling as HTMLElement;
708
+ if (!target) target = el;
709
+ if (target == el) instance.needsBeacon = true;
710
+ instance.relatedElement = target;
711
+ instance.setState({ dummy: {} });
712
+ }}
713
+ />
714
+ );
715
+ }
716
+ return [beacon, instance.relatedElement && super.render(context, instance, key)];
717
+ }
718
+
719
+ getOverlayContainer(): HTMLElement {
720
+ // this should be instance.relatedElement
721
+ if (this.relatedElement) {
722
+ let container = closestParent(
723
+ this.relatedElement,
724
+ (el) => el.dataset && !!el.dataset.focusableOverlayContainer,
725
+ );
726
+ if (container) return container;
727
+ }
728
+ return super.getOverlayContainer();
729
+ }
730
+ }
731
+
732
+ DropdownBase.prototype.offset = 0;
733
+ DropdownBase.prototype.baseClass = "dropdown";
734
+ DropdownBase.prototype.matchWidth = true;
735
+ DropdownBase.prototype.matchMaxWidth = false;
736
+ DropdownBase.prototype.placementOrder = "up down right left";
737
+ DropdownBase.prototype.placement = null; //default placement
738
+ DropdownBase.prototype.constrain = false;
739
+ DropdownBase.prototype.positioning = "fixed";
740
+ DropdownBase.prototype.touchFriendly = false;
741
+ DropdownBase.prototype.arrow = false;
742
+ DropdownBase.prototype.pad = false;
743
+ DropdownBase.prototype.elementExplode = 0;
744
+ DropdownBase.prototype.closeOnScrollDistance = 50;
745
+ DropdownBase.prototype.screenPadding = 5;
746
+ DropdownBase.prototype.firstChildDefinesHeight = false;
747
+ DropdownBase.prototype.firstChildDefinesWidth = false;
748
+ DropdownBase.prototype.cover = false;
749
+
750
+ export class Dropdown extends DropdownBase<DropdownConfig, DropdownInstance> {}
751
+
752
+ Widget.alias("dropdown", Dropdown);
753
+ Localization.registerPrototype("cx/widgets/Dropdown", Dropdown);
754
+
755
+ function getViewportRect(padding = 0) {
756
+ return {
757
+ left: padding,
758
+ top: padding,
759
+ right: document.documentElement.offsetWidth - padding,
760
+ bottom: document.documentElement.offsetHeight - padding,
761
+ };
762
+ }