@carto/ps-react-ui 4.3.3 → 4.3.5

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 (301) hide show
  1. package/dist/components.js +3 -3
  2. package/dist/components.js.map +1 -1
  3. package/dist/{lasso-tool-BwRzEW7k.js → lasso-tool-wFqOD6wk.js} +13 -13
  4. package/dist/lasso-tool-wFqOD6wk.js.map +1 -0
  5. package/dist/types/components/common-types.d.ts +41 -0
  6. package/dist/types/components/types.d.ts +1 -1
  7. package/dist/types/widgets/echart/echart-ui.d.ts +1 -1
  8. package/dist/types/widgets/echart/types.d.ts +4 -0
  9. package/dist/widgets/actions.js +1 -1
  10. package/dist/widgets/bar.js +1 -1
  11. package/dist/widgets/category.js +1 -1
  12. package/dist/widgets/echart.js +96 -85
  13. package/dist/widgets/echart.js.map +1 -1
  14. package/dist/widgets/formula.js +1 -1
  15. package/dist/widgets/histogram.js +1 -1
  16. package/dist/widgets/markdown.js +1 -1
  17. package/dist/widgets/pie.js +1 -1
  18. package/dist/widgets/scatterplot.js +1 -1
  19. package/dist/widgets/spread.js +1 -1
  20. package/dist/widgets/table.js +1 -1
  21. package/dist/widgets/timeseries.js +1 -1
  22. package/dist/widgets/toolbar-actions.js.map +1 -1
  23. package/dist/widgets/wrapper.js +1 -1
  24. package/package.json +8 -3
  25. package/src/components/basemaps/basemaps.test.tsx +196 -0
  26. package/src/components/basemaps/basemaps.tsx +128 -0
  27. package/src/components/basemaps/const.ts +13 -0
  28. package/src/components/basemaps/group-wrapper.test.tsx +38 -0
  29. package/src/components/basemaps/group-wrapper.tsx +28 -0
  30. package/src/components/basemaps/group.test.tsx +52 -0
  31. package/src/components/basemaps/group.tsx +42 -0
  32. package/src/components/basemaps/header.test.tsx +54 -0
  33. package/src/components/basemaps/header.tsx +36 -0
  34. package/src/components/basemaps/styles.ts +76 -0
  35. package/src/components/basemaps/types.ts +30 -0
  36. package/src/components/common-types.ts +1 -0
  37. package/src/components/geolocation-controls/const.ts +6 -0
  38. package/src/components/geolocation-controls/geolocation-controls.test.tsx +133 -0
  39. package/src/components/geolocation-controls/geolocation-controls.tsx +95 -0
  40. package/src/components/geolocation-controls/types.ts +17 -0
  41. package/src/components/index.ts +64 -0
  42. package/src/components/lasso-tool/chip.tsx +37 -0
  43. package/src/components/lasso-tool/const.tsx +70 -0
  44. package/src/components/lasso-tool/icons.tsx +91 -0
  45. package/src/components/lasso-tool/lasso-tool-inline.test.tsx +168 -0
  46. package/src/components/lasso-tool/lasso-tool-inline.tsx +245 -0
  47. package/src/components/lasso-tool/lasso-tool.test.tsx +212 -0
  48. package/src/components/lasso-tool/lasso-tool.tsx +479 -0
  49. package/src/components/lasso-tool/styles.ts +143 -0
  50. package/src/components/lasso-tool/types.ts +114 -0
  51. package/src/components/list-data/list-data-skeleton.test.tsx +10 -0
  52. package/src/components/list-data/list-data-skeleton.tsx +40 -0
  53. package/src/components/list-data/list-data.test.tsx +94 -0
  54. package/src/components/list-data/list-data.tsx +106 -0
  55. package/src/components/list-data/styles.ts +37 -0
  56. package/src/components/list-data/types.ts +25 -0
  57. package/src/components/measurement-tools/const.tsx +108 -0
  58. package/src/components/measurement-tools/icons.tsx +54 -0
  59. package/src/components/measurement-tools/measurement-tools.test.tsx +165 -0
  60. package/src/components/measurement-tools/measurement-tools.tsx +443 -0
  61. package/src/components/measurement-tools/styles.ts +91 -0
  62. package/src/components/measurement-tools/types.ts +77 -0
  63. package/src/components/no-data-alert/no-data-alert.test.tsx +31 -0
  64. package/src/components/no-data-alert/no-data-alert.tsx +59 -0
  65. package/src/components/responsive-drawer/responsive-drawer.test.tsx +91 -0
  66. package/src/components/responsive-drawer/responsive-drawer.tsx +53 -0
  67. package/src/components/smart-tooltip/smart-tooltip.test.tsx +168 -0
  68. package/src/components/smart-tooltip/smart-tooltip.tsx +40 -0
  69. package/src/components/tooltip/tooltip.test.tsx +86 -0
  70. package/src/components/tooltip/tooltip.tsx +30 -0
  71. package/src/components/types.ts +1 -0
  72. package/src/components/zoom-controls/styles.ts +27 -0
  73. package/src/components/zoom-controls/types.ts +19 -0
  74. package/src/components/zoom-controls/zoom-controls.test.tsx +101 -0
  75. package/src/components/zoom-controls/zoom-controls.tsx +114 -0
  76. package/src/hooks/index.ts +2 -0
  77. package/src/hooks/use-debounce.ts +55 -0
  78. package/src/hooks/use-skeleton.test.tsx +32 -0
  79. package/src/hooks/use-skeleton.ts +19 -0
  80. package/src/hooks/use-widget-ref.ts +33 -0
  81. package/src/widgets/README.md +277 -0
  82. package/src/widgets/_shared/chart-config/config-factory.ts +67 -0
  83. package/src/widgets/_shared/chart-config/csv-modifiers.ts +56 -0
  84. package/src/widgets/_shared/chart-config/index.ts +21 -0
  85. package/src/widgets/_shared/chart-config/option-builders.ts +203 -0
  86. package/src/widgets/_shared/skeleton/index.ts +5 -0
  87. package/src/widgets/_shared/skeleton/styles.ts +20 -0
  88. package/src/widgets/actions/change-column/change-column-icon.tsx +10 -0
  89. package/src/widgets/actions/change-column/change-column.test.tsx +163 -0
  90. package/src/widgets/actions/change-column/change-column.tsx +141 -0
  91. package/src/widgets/actions/change-column/sortable-column-item.tsx +49 -0
  92. package/src/widgets/actions/change-column/types.ts +20 -0
  93. package/src/widgets/actions/download/download.test.tsx +322 -0
  94. package/src/widgets/actions/download/download.tsx +118 -0
  95. package/src/widgets/actions/download/exports.test.tsx +275 -0
  96. package/src/widgets/actions/download/exports.tsx +103 -0
  97. package/src/widgets/actions/download/types.ts +21 -0
  98. package/src/widgets/actions/fullscreen/fullscreen.test.tsx +269 -0
  99. package/src/widgets/actions/fullscreen/fullscreen.tsx +82 -0
  100. package/src/widgets/actions/fullscreen/styles.ts +17 -0
  101. package/src/widgets/actions/fullscreen/types.ts +27 -0
  102. package/src/widgets/actions/index.ts +51 -0
  103. package/src/widgets/actions/lock-selection/lock-selection.test.tsx +186 -0
  104. package/src/widgets/actions/lock-selection/lock-selection.tsx +133 -0
  105. package/src/widgets/actions/lock-selection/types.ts +41 -0
  106. package/src/widgets/actions/relative-data/relative-data.test.tsx +267 -0
  107. package/src/widgets/actions/relative-data/relative-data.tsx +133 -0
  108. package/src/widgets/actions/relative-data/style.ts +9 -0
  109. package/src/widgets/actions/relative-data/types.ts +31 -0
  110. package/src/widgets/actions/relative-data/utils.test.ts +223 -0
  111. package/src/widgets/actions/relative-data/utils.ts +58 -0
  112. package/src/widgets/actions/searcher/searcher-toggle.test.tsx +354 -0
  113. package/src/widgets/actions/searcher/searcher-toggle.tsx +73 -0
  114. package/src/widgets/actions/searcher/searcher.tsx +205 -0
  115. package/src/widgets/actions/searcher/types.ts +72 -0
  116. package/src/widgets/actions/shared/styles.ts +12 -0
  117. package/src/widgets/actions/stack-toggle/grouped-bar-chart-icon.tsx +14 -0
  118. package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +270 -0
  119. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +146 -0
  120. package/src/widgets/actions/stack-toggle/types.ts +29 -0
  121. package/src/widgets/actions/zoom-toggle/index.ts +2 -0
  122. package/src/widgets/actions/zoom-toggle/style.ts +14 -0
  123. package/src/widgets/actions/zoom-toggle/types.ts +44 -0
  124. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +186 -0
  125. package/src/widgets/bar/config.ts +122 -0
  126. package/src/widgets/bar/index.ts +9 -0
  127. package/src/widgets/bar/skeleton.tsx +60 -0
  128. package/src/widgets/bar/style.ts +33 -0
  129. package/src/widgets/bar/types.ts +16 -0
  130. package/src/widgets/category/category-ui.test.tsx +399 -0
  131. package/src/widgets/category/category-ui.tsx +156 -0
  132. package/src/widgets/category/components/category-bar.tsx +28 -0
  133. package/src/widgets/category/components/category-legend.tsx +30 -0
  134. package/src/widgets/category/components/category-row-multi.tsx +50 -0
  135. package/src/widgets/category/components/category-row-other.tsx +23 -0
  136. package/src/widgets/category/components/category-row-single.tsx +47 -0
  137. package/src/widgets/category/components/index.ts +14 -0
  138. package/src/widgets/category/config.ts +85 -0
  139. package/src/widgets/category/index.ts +30 -0
  140. package/src/widgets/category/skeleton.tsx +24 -0
  141. package/src/widgets/category/style.ts +133 -0
  142. package/src/widgets/category/types.ts +40 -0
  143. package/src/widgets/echart/const.ts +1 -0
  144. package/src/widgets/echart/echart-ui.test.tsx +537 -0
  145. package/src/widgets/echart/echart-ui.tsx +92 -0
  146. package/src/widgets/echart/echart.test.tsx +562 -0
  147. package/src/widgets/echart/echart.tsx +68 -0
  148. package/src/widgets/echart/index.ts +16 -0
  149. package/src/widgets/echart/options.ts +53 -0
  150. package/src/widgets/echart/types.ts +45 -0
  151. package/src/widgets/echart/utils.ts +169 -0
  152. package/src/widgets/error/error.test.tsx +331 -0
  153. package/src/widgets/error/error.tsx +40 -0
  154. package/src/widgets/error/index.ts +2 -0
  155. package/src/widgets/error/types.ts +14 -0
  156. package/src/widgets/formula/components/item.test.tsx +249 -0
  157. package/src/widgets/formula/components/item.tsx +18 -0
  158. package/src/widgets/formula/components/prefix.test.tsx +341 -0
  159. package/src/widgets/formula/components/prefix.tsx +18 -0
  160. package/src/widgets/formula/components/row.test.tsx +364 -0
  161. package/src/widgets/formula/components/row.tsx +21 -0
  162. package/src/widgets/formula/components/series.tsx +34 -0
  163. package/src/widgets/formula/components/suffix.test.tsx +383 -0
  164. package/src/widgets/formula/components/suffix.tsx +28 -0
  165. package/src/widgets/formula/components/value.test.tsx +329 -0
  166. package/src/widgets/formula/components/value.tsx +29 -0
  167. package/src/widgets/formula/config.ts +27 -0
  168. package/src/widgets/formula/formula-ui.test.tsx +399 -0
  169. package/src/widgets/formula/formula-ui.tsx +27 -0
  170. package/src/widgets/formula/index.ts +24 -0
  171. package/src/widgets/formula/serializer.test.tsx +144 -0
  172. package/src/widgets/formula/serializer.ts +28 -0
  173. package/src/widgets/formula/skeleton.tsx +10 -0
  174. package/src/widgets/formula/style.ts +23 -0
  175. package/src/widgets/formula/types.ts +50 -0
  176. package/src/widgets/histogram/config.ts +143 -0
  177. package/src/widgets/histogram/index.ts +8 -0
  178. package/src/widgets/histogram/skeleton.tsx +52 -0
  179. package/src/widgets/histogram/style.ts +8 -0
  180. package/src/widgets/histogram/types.ts +17 -0
  181. package/src/widgets/index.ts +25 -0
  182. package/src/widgets/loader/index.ts +4 -0
  183. package/src/widgets/loader/loader.tsx +70 -0
  184. package/src/widgets/loader/types.ts +11 -0
  185. package/src/widgets/loader/utils.test.ts +112 -0
  186. package/src/widgets/loader/utils.ts +35 -0
  187. package/src/widgets/markdown/config.ts +18 -0
  188. package/src/widgets/markdown/index.ts +14 -0
  189. package/src/widgets/markdown/markdown-ui.test.tsx +341 -0
  190. package/src/widgets/markdown/markdown-ui.tsx +52 -0
  191. package/src/widgets/markdown/markdown.tsx +20 -0
  192. package/src/widgets/markdown/skeleton.tsx +12 -0
  193. package/src/widgets/markdown/style.ts +28 -0
  194. package/src/widgets/markdown/types.ts +28 -0
  195. package/src/widgets/no-data/index.ts +2 -0
  196. package/src/widgets/no-data/no-data.test.tsx +447 -0
  197. package/src/widgets/no-data/no-data.tsx +116 -0
  198. package/src/widgets/no-data/style.ts +18 -0
  199. package/src/widgets/no-data/types.ts +72 -0
  200. package/src/widgets/note/index.ts +2 -0
  201. package/src/widgets/note/note.test.tsx +391 -0
  202. package/src/widgets/note/note.tsx +114 -0
  203. package/src/widgets/note/style.ts +29 -0
  204. package/src/widgets/note/types.ts +9 -0
  205. package/src/widgets/pie/config.ts +177 -0
  206. package/src/widgets/pie/index.ts +8 -0
  207. package/src/widgets/pie/skeleton.tsx +70 -0
  208. package/src/widgets/pie/style.ts +8 -0
  209. package/src/widgets/pie/types.ts +16 -0
  210. package/src/widgets/range/components/range-item.tsx +213 -0
  211. package/src/widgets/range/config.ts +10 -0
  212. package/src/widgets/range/index.ts +16 -0
  213. package/src/widgets/range/range-ui.test.tsx +203 -0
  214. package/src/widgets/range/range-ui.tsx +11 -0
  215. package/src/widgets/range/serializer.test.ts +70 -0
  216. package/src/widgets/range/serializer.ts +27 -0
  217. package/src/widgets/range/skeleton.tsx +14 -0
  218. package/src/widgets/range/style.ts +37 -0
  219. package/src/widgets/range/types.ts +39 -0
  220. package/src/widgets/scatterplot/config.ts +138 -0
  221. package/src/widgets/scatterplot/index.ts +8 -0
  222. package/src/widgets/scatterplot/skeleton.tsx +59 -0
  223. package/src/widgets/scatterplot/style.ts +21 -0
  224. package/src/widgets/scatterplot/types.ts +17 -0
  225. package/src/widgets/selection-summary/index.ts +6 -0
  226. package/src/widgets/selection-summary/selection-summary.tsx +46 -0
  227. package/src/widgets/selection-summary/style.ts +10 -0
  228. package/src/widgets/selection-summary/types.ts +14 -0
  229. package/src/widgets/skeleton-loader/index.ts +2 -0
  230. package/src/widgets/skeleton-loader/skeleton-loader.test.tsx +139 -0
  231. package/src/widgets/skeleton-loader/skeleton-loader.tsx +28 -0
  232. package/src/widgets/skeleton-loader/types.ts +8 -0
  233. package/src/widgets/spread/components/max-value.tsx +29 -0
  234. package/src/widgets/spread/components/min-value.tsx +29 -0
  235. package/src/widgets/spread/components/separator.tsx +6 -0
  236. package/src/widgets/spread/config.ts +34 -0
  237. package/src/widgets/spread/index.ts +23 -0
  238. package/src/widgets/spread/skeleton.tsx +10 -0
  239. package/src/widgets/spread/spread-ui.test.tsx +368 -0
  240. package/src/widgets/spread/spread-ui.tsx +29 -0
  241. package/src/widgets/spread/style.ts +22 -0
  242. package/src/widgets/spread/types.ts +47 -0
  243. package/src/widgets/stores/index.ts +9 -0
  244. package/src/widgets/stores/types.ts +192 -0
  245. package/src/widgets/stores/widget-store.test.ts +601 -0
  246. package/src/widgets/stores/widget-store.ts +239 -0
  247. package/src/widgets/subheader/index.ts +3 -0
  248. package/src/widgets/subheader/style.ts +20 -0
  249. package/src/widgets/subheader/subheader.test.tsx +45 -0
  250. package/src/widgets/subheader/subheader.tsx +16 -0
  251. package/src/widgets/subheader/types.ts +11 -0
  252. package/src/widgets/table/components/cell-header.tsx +58 -0
  253. package/src/widgets/table/components/cell.tsx +80 -0
  254. package/src/widgets/table/components/index.ts +4 -0
  255. package/src/widgets/table/components/pagination-actions.tsx +67 -0
  256. package/src/widgets/table/components/pagination.tsx +41 -0
  257. package/src/widgets/table/components/row.tsx +60 -0
  258. package/src/widgets/table/config.ts +71 -0
  259. package/src/widgets/table/helpers.test.ts +244 -0
  260. package/src/widgets/table/helpers.ts +107 -0
  261. package/src/widgets/table/hooks/index.ts +7 -0
  262. package/src/widgets/table/hooks/use-pagination.test.ts +294 -0
  263. package/src/widgets/table/hooks/use-pagination.ts +155 -0
  264. package/src/widgets/table/hooks/use-selection.test.ts +504 -0
  265. package/src/widgets/table/hooks/use-selection.ts +189 -0
  266. package/src/widgets/table/hooks/use-sort.test.ts +296 -0
  267. package/src/widgets/table/hooks/use-sort.ts +138 -0
  268. package/src/widgets/table/index.ts +53 -0
  269. package/src/widgets/table/serializer.ts +54 -0
  270. package/src/widgets/table/skeleton.tsx +48 -0
  271. package/src/widgets/table/style.ts +34 -0
  272. package/src/widgets/table/table-ui.tsx +64 -0
  273. package/src/widgets/table/types.ts +223 -0
  274. package/src/widgets/timeseries/config.ts +135 -0
  275. package/src/widgets/timeseries/index.ts +8 -0
  276. package/src/widgets/timeseries/skeleton.tsx +55 -0
  277. package/src/widgets/timeseries/style.ts +36 -0
  278. package/src/widgets/timeseries/types.ts +17 -0
  279. package/src/widgets/toolbar-actions/index.ts +6 -0
  280. package/src/widgets/toolbar-actions/styles.ts +38 -0
  281. package/src/widgets/toolbar-actions/toolbar-actions.test.tsx +691 -0
  282. package/src/widgets/toolbar-actions/toolbar-actions.tsx +145 -0
  283. package/src/widgets/toolbar-actions/types.ts +60 -0
  284. package/src/widgets/wrapper/components/actions.test.tsx +101 -0
  285. package/src/widgets/wrapper/components/actions.tsx +30 -0
  286. package/src/widgets/wrapper/components/options.test.tsx +323 -0
  287. package/src/widgets/wrapper/components/options.tsx +73 -0
  288. package/src/widgets/wrapper/components/title.test.tsx +126 -0
  289. package/src/widgets/wrapper/components/title.tsx +32 -0
  290. package/src/widgets/wrapper/index.ts +16 -0
  291. package/src/widgets/wrapper/styles.ts +98 -0
  292. package/src/widgets/wrapper/types.ts +55 -0
  293. package/src/widgets/wrapper/wrapper-ui.test.tsx +232 -0
  294. package/src/widgets/wrapper/wrapper-ui.tsx +57 -0
  295. package/src/widgets/wrapper/wrapper.test.tsx +365 -0
  296. package/src/widgets/wrapper/wrapper.tsx +50 -0
  297. package/dist/lasso-tool-BwRzEW7k.js.map +0 -1
  298. package/dist/types/common/common.d.ts +0 -3
  299. package/dist/types/common/index.d.ts +0 -26
  300. package/dist/types/common/lasso-tools.d.ts +0 -36
  301. package/dist/types/common/measurement-tools.d.ts +0 -65
@@ -0,0 +1,145 @@
1
+ import { CloseOutlined } from '@mui/icons-material'
2
+ import { Box, Divider, Grow, IconButton, Paper } from '@mui/material'
3
+ import { isValidElement, useState, type JSX, type MouseEvent } from 'react'
4
+ import { Tooltip } from '../../components/tooltip/tooltip'
5
+ import { styles } from './styles'
6
+ import type { ToolbarActionsProps } from './types'
7
+ import { WidgetOptions } from '@carto/meridian-ds/custom-icons'
8
+
9
+ const DEFAULT_LABELS = {
10
+ trigger: 'More actions',
11
+ close: 'Close',
12
+ }
13
+
14
+ /**
15
+ * A floating toolbar component that displays action buttons.
16
+ * Shows a trigger button when there are more visible actions than the visibleCount,
17
+ * which expands to reveal all actions with a close button.
18
+ * When visibleCount is undefined, all actions are shown without overflow.
19
+ *
20
+ * Children with the `data-toolbar-hidden` attribute are ignored when counting
21
+ * visible actions but are included in the preview. This is useful for elements
22
+ * like Dividers that should appear between actions but not count toward the limit.
23
+ *
24
+ * Example: With visibleCount=2 and children [Action1, Divider, Action2, Divider, Action3]:
25
+ * - Preview shows: Action1, Divider, Action2 (2 visible actions + divider)
26
+ * - Hidden: Divider, Action3
27
+ */
28
+ export function ToolbarActions({
29
+ children: _children,
30
+ visibleCount,
31
+ direction,
32
+ labels,
33
+ sx,
34
+ IconButtonProps,
35
+ TooltipProps,
36
+ }: ToolbarActionsProps): JSX.Element | null {
37
+ const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
38
+
39
+ const triggerLabel = labels?.trigger ?? DEFAULT_LABELS.trigger
40
+ const closeLabel = labels?.close ?? DEFAULT_LABELS.close
41
+
42
+ const children = (Array.isArray(_children) ? _children : [_children]).filter(
43
+ Boolean,
44
+ )
45
+
46
+ // Don't render if no actions
47
+ if (children.length === 0) {
48
+ return null
49
+ }
50
+
51
+ // Filter out children with data-toolbar-hidden attribute for counting
52
+ const visibleChildren = children.filter(
53
+ (child) =>
54
+ !(
55
+ isValidElement<{ 'data-toolbar-hidden'?: boolean }>(child) &&
56
+ child.props['data-toolbar-hidden']
57
+ ),
58
+ )
59
+
60
+ // Show trigger only when visibleCount is defined and there are more visible actions
61
+ const showTrigger =
62
+ visibleCount !== undefined && visibleChildren.length > visibleCount
63
+
64
+ const handleToggle = (e: MouseEvent<HTMLElement>) => {
65
+ setAnchorEl((prev) => (prev ? null : e.currentTarget))
66
+ }
67
+
68
+ // Build preview by including hidden elements but only counting visible ones
69
+ const childrenPreview =
70
+ visibleCount === undefined
71
+ ? children
72
+ : children.reduce<{ result: React.ReactNode[]; count: number }>(
73
+ (acc, child) => {
74
+ // Stop if we've already collected enough visible actions
75
+ if (acc.count >= visibleCount) {
76
+ return acc
77
+ }
78
+
79
+ const isHidden =
80
+ isValidElement<{ 'data-toolbar-hidden'?: boolean }>(child) &&
81
+ child.props['data-toolbar-hidden']
82
+
83
+ return {
84
+ result: [...acc.result, child],
85
+ count: acc.count + (isHidden ? 0 : 1),
86
+ }
87
+ },
88
+ { result: [], count: 0 },
89
+ ).result
90
+
91
+ const width = anchorEl?.getBoundingClientRect().width
92
+
93
+ return (
94
+ <Box
95
+ sx={{
96
+ ...styles.root,
97
+ ...sx,
98
+ flexDirection: direction === 'left' ? 'row-reverse' : 'row',
99
+ }}
100
+ >
101
+ {/* Trigger button - only shown when 3+ actions */}
102
+ {showTrigger && (
103
+ <>
104
+ <Tooltip
105
+ title={anchorEl ? closeLabel : triggerLabel}
106
+ placement='top'
107
+ {...TooltipProps}
108
+ >
109
+ <IconButton
110
+ size='small'
111
+ aria-label={anchorEl ? closeLabel : triggerLabel}
112
+ onClick={handleToggle}
113
+ sx={styles.triggerButton}
114
+ data-active={!!anchorEl}
115
+ {...IconButtonProps}
116
+ >
117
+ {anchorEl ? <CloseOutlined /> : <WidgetOptions />}
118
+ </IconButton>
119
+ </Tooltip>
120
+ <Divider orientation='vertical' flexItem />
121
+ </>
122
+ )}
123
+
124
+ <Box sx={styles.preview}>{childrenPreview}</Box>
125
+
126
+ {/* Actions container - shown only when expanded (trigger is active) */}
127
+ <Grow in={!!anchorEl}>
128
+ <Paper
129
+ sx={{
130
+ ...styles.actionsContainer,
131
+ transform: `translate3d(${
132
+ direction === 'left' ? `-${width ?? 0}px` : `${width ?? 0}px`
133
+ }, 0, 0) !important`,
134
+ marginLeft: ({ spacing }) =>
135
+ direction === 'left' ? 0 : spacing(0.25),
136
+ marginRight: ({ spacing }) =>
137
+ direction === 'left' ? spacing(0.25) : 0,
138
+ }}
139
+ >
140
+ {children}
141
+ </Paper>
142
+ </Grow>
143
+ </Box>
144
+ )
145
+ }
@@ -0,0 +1,60 @@
1
+ import type {
2
+ IconButtonProps,
3
+ SxProps,
4
+ Theme,
5
+ TooltipProps,
6
+ } from '@mui/material'
7
+ import type { ReactNode } from 'react'
8
+
9
+ /**
10
+ * Represents a single action in the toolbar
11
+ */
12
+ export interface ToolbarAction {
13
+ /** Unique identifier for the action */
14
+ id: string
15
+ /** Icon to display for the action */
16
+ icon: ReactNode
17
+ /** Label displayed in tooltip */
18
+ label: string
19
+ /** Callback when action is clicked */
20
+ onClick: () => void
21
+ /** Whether the action is disabled */
22
+ disabled?: boolean
23
+ }
24
+
25
+ /**
26
+ * Labels for toolbar UI elements
27
+ */
28
+ export interface ToolbarActionsLabels {
29
+ /** Tooltip for the trigger button */
30
+ trigger?: string
31
+ /** Tooltip for the close button */
32
+ close?: string
33
+ }
34
+
35
+ /**
36
+ * Props for the ToolbarActions component
37
+ */
38
+ export interface ToolbarActionsProps {
39
+ /**
40
+ * Array of React elements (typically IconButtons) to display in the toolbar.
41
+ * Elements with `data-toolbar-hidden` attribute are ignored when counting visible
42
+ * actions and excluded from the collapsed preview (e.g., Dividers).
43
+ */
44
+ children: ReactNode[] | ReactNode
45
+ /** Direction in which the toolbar expands */
46
+ direction?: 'left' | 'right'
47
+ /**
48
+ * Number of visible actions to show in collapsed mode.
49
+ * When undefined, shows all actions without overflow.
50
+ */
51
+ visibleCount?: number
52
+ /** Custom labels for UI elements */
53
+ labels?: ToolbarActionsLabels
54
+ /** Custom styles for the root container */
55
+ sx?: SxProps<Theme>
56
+ /** Props passed to action IconButtons */
57
+ IconButtonProps?: Omit<IconButtonProps, 'onClick' | 'disabled'>
58
+ /** Props passed to Tooltips */
59
+ TooltipProps?: Omit<TooltipProps, 'title' | 'children'>
60
+ }
@@ -0,0 +1,101 @@
1
+ import { describe, test, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { Actions } from './actions'
4
+
5
+ describe('Actions', () => {
6
+ test('renders with empty actions array', () => {
7
+ const { container } = render(<Actions actions={[]} />)
8
+ expect(container.firstChild).toBeTruthy()
9
+ })
10
+
11
+ test('renders with undefined actions', () => {
12
+ const { container } = render(<Actions />)
13
+ expect(container.firstChild).toBeTruthy()
14
+ })
15
+
16
+ test('renders single action', () => {
17
+ const actions = [<button key='action1'>Action 1</button>]
18
+ render(<Actions actions={actions} />)
19
+ expect(screen.getByText('Action 1')).toBeTruthy()
20
+ })
21
+
22
+ test('renders multiple actions', () => {
23
+ const actions = [
24
+ <button key='action1'>Action 1</button>,
25
+ <button key='action2'>Action 2</button>,
26
+ <button key='action3'>Action 3</button>,
27
+ ]
28
+ render(<Actions actions={actions} />)
29
+ expect(screen.getByText('Action 1')).toBeTruthy()
30
+ expect(screen.getByText('Action 2')).toBeTruthy()
31
+ expect(screen.getByText('Action 3')).toBeTruthy()
32
+ })
33
+
34
+ test('stops event propagation on click', () => {
35
+ const onClick = vi.fn()
36
+ const parentOnClick = vi.fn()
37
+
38
+ const actions = [
39
+ <button key='action1' onClick={onClick}>
40
+ Action 1
41
+ </button>,
42
+ ]
43
+
44
+ render(
45
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
46
+ <div onClick={parentOnClick}>
47
+ <Actions actions={actions} />
48
+ </div>,
49
+ )
50
+
51
+ const button = screen.getByText('Action 1')
52
+ fireEvent.click(button)
53
+
54
+ // The action's onClick should fire
55
+ expect(onClick).toHaveBeenCalledTimes(1)
56
+ // But parent onClick should not fire due to stopPropagation
57
+ expect(parentOnClick).not.toHaveBeenCalled()
58
+ })
59
+
60
+ test('renders actions with different types of elements', () => {
61
+ const actions = [
62
+ <button key='button'>Button</button>,
63
+ <button key='link'>Link</button>,
64
+ <span key='span'>Span</span>,
65
+ ]
66
+ render(<Actions actions={actions} />)
67
+ expect(screen.getByText('Button')).toBeTruthy()
68
+ expect(screen.getByText('Link')).toBeTruthy()
69
+ expect(screen.getByText('Span')).toBeTruthy()
70
+ })
71
+
72
+ test('wraps each action in a Box with correct key', () => {
73
+ const actions = [
74
+ <button key='action1'>Action 1</button>,
75
+ <button key='action2'>Action 2</button>,
76
+ ]
77
+ const { container } = render(<Actions actions={actions} />)
78
+
79
+ // Check that actions are rendered
80
+ expect(screen.getByText('Action 1')).toBeTruthy()
81
+ expect(screen.getByText('Action 2')).toBeTruthy()
82
+
83
+ // The container should have the actions box
84
+ expect(container.firstChild).toBeTruthy()
85
+ })
86
+
87
+ test('handles complex action components', () => {
88
+ const ComplexAction = () => (
89
+ <div>
90
+ <span>Complex</span>
91
+ <button>Action</button>
92
+ </div>
93
+ )
94
+
95
+ const actions = [<ComplexAction key='complex' />]
96
+ render(<Actions actions={actions} />)
97
+
98
+ expect(screen.getByText('Complex')).toBeTruthy()
99
+ expect(screen.getByText('Action')).toBeTruthy()
100
+ })
101
+ })
@@ -0,0 +1,30 @@
1
+ import { Box } from '@mui/material'
2
+ import type { WrapperActionsProps } from '../types'
3
+ import { styles } from '../styles'
4
+
5
+ export function Actions({ actions = [] }: WrapperActionsProps) {
6
+ return (
7
+ <Box sx={styles.actions} className='widget-wrapper-actions'>
8
+ {actions.map((action, index) => {
9
+ // Prefer action.key if present (React elements), else fallback to index
10
+ const key =
11
+ action &&
12
+ typeof action === 'object' &&
13
+ 'key' in action &&
14
+ action.key != null
15
+ ? action.key
16
+ : index
17
+ return (
18
+ <Box
19
+ key={key}
20
+ onClick={(e) => {
21
+ e.stopPropagation()
22
+ }}
23
+ >
24
+ {action}
25
+ </Box>
26
+ )
27
+ })}
28
+ </Box>
29
+ )
30
+ }
@@ -0,0 +1,323 @@
1
+ import { describe, test, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { Options } from './options'
4
+
5
+ describe('Options', () => {
6
+ test('renders options button with default label', () => {
7
+ const options = [
8
+ {
9
+ label: 'Option 1',
10
+ onClick: vi.fn(),
11
+ },
12
+ ]
13
+ render(<Options options={options} />)
14
+ expect(screen.getByLabelText('Options')).toBeTruthy()
15
+ })
16
+
17
+ test('renders options button with custom label', () => {
18
+ const options = [
19
+ {
20
+ label: 'Option 1',
21
+ onClick: vi.fn(),
22
+ },
23
+ ]
24
+ const labels = {
25
+ title: 'Custom Options Label',
26
+ }
27
+ render(<Options options={options} labels={labels} />)
28
+ expect(screen.getByLabelText('Custom Options Label')).toBeTruthy()
29
+ })
30
+
31
+ test('renders when options array is empty', () => {
32
+ render(<Options options={[]} />)
33
+ // Component still renders button even with empty options
34
+ // (In practice, WrapperUI doesn't render Options when options.length === 0)
35
+ expect(screen.getByLabelText('Options')).toBeTruthy()
36
+ })
37
+
38
+ test('renders when options is undefined', () => {
39
+ render(<Options />)
40
+ // Component still renders button even with undefined options
41
+ // (In practice, WrapperUI doesn't render Options when options.length === 0)
42
+ expect(screen.getByLabelText('Options')).toBeTruthy()
43
+ })
44
+
45
+ test('opens menu when button is clicked', () => {
46
+ const options = [
47
+ {
48
+ label: 'Option 1',
49
+ onClick: vi.fn(),
50
+ },
51
+ ]
52
+ render(<Options options={options} />)
53
+
54
+ const button = screen.getByLabelText('Options')
55
+ fireEvent.click(button)
56
+
57
+ expect(screen.getByText('Option 1')).toBeTruthy()
58
+ })
59
+
60
+ test('closes menu when option is clicked', () => {
61
+ const onClick = vi.fn()
62
+ const options = [
63
+ {
64
+ label: 'Option 1',
65
+ onClick,
66
+ },
67
+ ]
68
+ render(<Options options={options} />)
69
+
70
+ const button = screen.getByLabelText('Options')
71
+ fireEvent.click(button)
72
+
73
+ const menuItem = screen.getByText('Option 1')
74
+ fireEvent.click(menuItem)
75
+
76
+ expect(onClick).toHaveBeenCalledTimes(1)
77
+ // Menu should be closed after clicking
78
+ expect(screen.queryByRole('menu')).toBeNull()
79
+ })
80
+
81
+ test('stops event propagation when opening menu', () => {
82
+ const parentOnClick = vi.fn()
83
+ const options = [
84
+ {
85
+ label: 'Option 1',
86
+ onClick: vi.fn(),
87
+ },
88
+ ]
89
+
90
+ render(
91
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
92
+ <div onClick={parentOnClick}>
93
+ <Options options={options} />
94
+ </div>,
95
+ )
96
+
97
+ const button = screen.getByLabelText('Options')
98
+ fireEvent.click(button)
99
+
100
+ // Parent onClick should not be called
101
+ expect(parentOnClick).not.toHaveBeenCalled()
102
+ })
103
+
104
+ test('stops event propagation when clicking option', () => {
105
+ const parentOnClick = vi.fn()
106
+ const onClick = vi.fn()
107
+ const options = [
108
+ {
109
+ label: 'Option 1',
110
+ onClick,
111
+ },
112
+ ]
113
+
114
+ render(
115
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
116
+ <div onClick={parentOnClick}>
117
+ <Options options={options} />
118
+ </div>,
119
+ )
120
+
121
+ const button = screen.getByLabelText('Options')
122
+ fireEvent.click(button)
123
+
124
+ const menuItem = screen.getByText('Option 1')
125
+ fireEvent.click(menuItem)
126
+
127
+ expect(onClick).toHaveBeenCalledTimes(1)
128
+ expect(parentOnClick).not.toHaveBeenCalled()
129
+ })
130
+
131
+ test('renders multiple options', () => {
132
+ const options = [
133
+ {
134
+ label: 'Option 1',
135
+ onClick: vi.fn(),
136
+ },
137
+ {
138
+ label: 'Option 2',
139
+ onClick: vi.fn(),
140
+ },
141
+ {
142
+ label: 'Option 3',
143
+ onClick: vi.fn(),
144
+ },
145
+ ]
146
+ render(<Options options={options} />)
147
+
148
+ const button = screen.getByLabelText('Options')
149
+ fireEvent.click(button)
150
+
151
+ expect(screen.getByText('Option 1')).toBeTruthy()
152
+ expect(screen.getByText('Option 2')).toBeTruthy()
153
+ expect(screen.getByText('Option 3')).toBeTruthy()
154
+ })
155
+
156
+ test('renders disabled option', () => {
157
+ const options = [
158
+ {
159
+ label: 'Disabled Option',
160
+ disabled: true,
161
+ onClick: vi.fn(),
162
+ },
163
+ ]
164
+ render(<Options options={options} />)
165
+
166
+ const button = screen.getByLabelText('Options')
167
+ fireEvent.click(button)
168
+
169
+ const menuItem = screen.getByText('Disabled Option').closest('li')
170
+ expect(menuItem?.classList.contains('Mui-disabled')).toBe(true)
171
+ })
172
+
173
+ test('does not call onClick for disabled option', () => {
174
+ const onClick = vi.fn()
175
+ const options = [
176
+ {
177
+ label: 'Enabled Option',
178
+ onClick,
179
+ },
180
+ {
181
+ label: 'Disabled Option',
182
+ disabled: true,
183
+ onClick,
184
+ },
185
+ ]
186
+ render(<Options options={options} />)
187
+
188
+ const button = screen.getByLabelText('Options')
189
+ fireEvent.click(button)
190
+
191
+ // Click on enabled option first
192
+ const enabled = screen.getByText('Enabled Option')
193
+ fireEvent.click(enabled)
194
+
195
+ // onClick should be called once for enabled
196
+ expect(onClick).toHaveBeenCalledTimes(1)
197
+
198
+ // Open menu again and try clicking disabled
199
+ fireEvent.click(button)
200
+ const disabled = screen.getByText('Disabled Option').closest('li')
201
+
202
+ // Disabled should have the class
203
+ expect(disabled?.classList.contains('Mui-disabled')).toBe(true)
204
+ })
205
+
206
+ test('renders option with icon', () => {
207
+ const icon = <span data-testid='test-icon'>Icon</span>
208
+ const options = [
209
+ {
210
+ label: 'Option with Icon',
211
+ icon,
212
+ onClick: vi.fn(),
213
+ },
214
+ ]
215
+ render(<Options options={options} />)
216
+
217
+ const button = screen.getByLabelText('Options')
218
+ fireEvent.click(button)
219
+
220
+ expect(screen.getByTestId('test-icon')).toBeTruthy()
221
+ expect(screen.getByText('Option with Icon')).toBeTruthy()
222
+ })
223
+
224
+ test('renders option without icon', () => {
225
+ const options = [
226
+ {
227
+ label: 'Option without Icon',
228
+ onClick: vi.fn(),
229
+ },
230
+ ]
231
+ render(<Options options={options} />)
232
+
233
+ const button = screen.getByLabelText('Options')
234
+ fireEvent.click(button)
235
+
236
+ expect(screen.getByText('Option without Icon')).toBeTruthy()
237
+ })
238
+
239
+ test('closes menu when clicking outside', () => {
240
+ const options = [
241
+ {
242
+ label: 'Option 1',
243
+ onClick: vi.fn(),
244
+ },
245
+ ]
246
+ render(<Options options={options} />)
247
+
248
+ const button = screen.getByLabelText('Options')
249
+ fireEvent.click(button)
250
+
251
+ // Menu should be open
252
+ expect(screen.getByText('Option 1')).toBeTruthy()
253
+
254
+ // Click outside (on the backdrop)
255
+ const backdrop = document.querySelector('.MuiBackdrop-root')
256
+ if (backdrop) {
257
+ fireEvent.click(backdrop)
258
+ }
259
+
260
+ // Menu should be closed
261
+ expect(screen.queryByRole('menu')).toBeNull()
262
+ })
263
+
264
+ test('handles multiple option clicks correctly', () => {
265
+ const onClick1 = vi.fn()
266
+ const onClick2 = vi.fn()
267
+ const options = [
268
+ {
269
+ label: 'Option 1',
270
+ onClick: onClick1,
271
+ },
272
+ {
273
+ label: 'Option 2',
274
+ onClick: onClick2,
275
+ },
276
+ ]
277
+ render(<Options options={options} />)
278
+
279
+ // Open menu and click first option
280
+ const button = screen.getByLabelText('Options')
281
+ fireEvent.click(button)
282
+ fireEvent.click(screen.getByText('Option 1'))
283
+
284
+ expect(onClick1).toHaveBeenCalledTimes(1)
285
+ expect(onClick2).not.toHaveBeenCalled()
286
+
287
+ // Open menu again and click second option
288
+ fireEvent.click(button)
289
+ fireEvent.click(screen.getByText('Option 2'))
290
+
291
+ expect(onClick1).toHaveBeenCalledTimes(1)
292
+ expect(onClick2).toHaveBeenCalledTimes(1)
293
+ })
294
+
295
+ test('renders MoreVert icon', () => {
296
+ const options = [
297
+ {
298
+ label: 'Option 1',
299
+ onClick: vi.fn(),
300
+ },
301
+ ]
302
+ render(<Options options={options} />)
303
+
304
+ // Check that the button is rendered
305
+ const button = screen.getByLabelText('Options')
306
+ expect(button).toBeTruthy()
307
+ })
308
+
309
+ test('menu has correct attributes', () => {
310
+ const options = [
311
+ {
312
+ label: 'Option 1',
313
+ onClick: vi.fn(),
314
+ },
315
+ ]
316
+ render(<Options options={options} />)
317
+
318
+ const button = screen.getByLabelText('Options')
319
+ expect(button.getAttribute('aria-label')).toBe('Options')
320
+ expect(button.getAttribute('aria-controls')).toBe('options-menu')
321
+ expect(button.getAttribute('aria-haspopup')).toBe('true')
322
+ })
323
+ })