@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
@@ -3,7 +3,7 @@ import { c as _ } from "react/compiler-runtime";
3
3
  import { Box as O, IconButton as L, MenuItem as P, ListItemIcon as j, ListItemText as z, Menu as D, Typography as H, LinearProgress as N, AccordionSummary as G, AccordionDetails as U, Accordion as V } from "@mui/material";
4
4
  import { MoreVert as q } from "@mui/icons-material";
5
5
  import { useState as J, useEffect as K } from "react";
6
- import "../lasso-tool-BwRzEW7k.js";
6
+ import "../lasso-tool-wFqOD6wk.js";
7
7
  import "../cjs-D4KH3azB.js";
8
8
  import { S as Q } from "../smart-tooltip-BEtBaIdz.js";
9
9
  import { u as T } from "../widget-store-CB6Trp_0.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carto/ps-react-ui",
3
- "version": "4.3.3",
3
+ "version": "4.3.5",
4
4
  "description": "CARTO's Professional Service React Material library",
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -15,6 +15,7 @@
15
15
  "html2canvas": "1.4.1",
16
16
  "react-markdown": "10.1.0",
17
17
  "zustand": "5.0.11",
18
+ "@carto/ps-common-types": "1.0.0",
18
19
  "@carto/ps-utils": "2.0.1"
19
20
  },
20
21
  "peerDependencies": {
@@ -134,10 +135,14 @@
134
135
  }
135
136
  },
136
137
  "publishConfig": {
137
- "access": "public"
138
+ "access": "public",
139
+ "files": [
140
+ "dist"
141
+ ]
138
142
  },
139
143
  "files": [
140
- "dist"
144
+ "dist",
145
+ "src"
141
146
  ],
142
147
  "keywords": [
143
148
  "carto",
@@ -0,0 +1,196 @@
1
+ import { describe, test, expect, vi, beforeEach } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { BasemapsUI } from './basemaps'
4
+ import type { BasemapOption } from './types'
5
+
6
+ // Mock the child components
7
+ vi.mock('./header', () => ({
8
+ Header: ({ onChangeCollapsed }: { onChangeCollapsed: () => void }) => (
9
+ <div data-testid='mock-header'>
10
+ <button data-testid='close-button' onClick={onChangeCollapsed}>
11
+ Close
12
+ </button>
13
+ </div>
14
+ ),
15
+ }))
16
+
17
+ vi.mock('./group', () => ({
18
+ Group: ({
19
+ values,
20
+ selected,
21
+ onChange,
22
+ }: {
23
+ values: BasemapOption[]
24
+ selected: string | null
25
+ onChange: (id: string) => void
26
+ }) => (
27
+ <div data-testid='mock-group'>
28
+ {values.map((option) => (
29
+ <button
30
+ key={option.id}
31
+ data-testid={`basemap-option-${option.id}`}
32
+ data-selected={option.id === selected}
33
+ onClick={() => onChange(option.id)}
34
+ >
35
+ {option.label}
36
+ </button>
37
+ ))}
38
+ </div>
39
+ ),
40
+ }))
41
+
42
+ vi.mock('./group-wrapper', () => ({
43
+ GroupWrapper: ({
44
+ title,
45
+ children,
46
+ }: {
47
+ title: string
48
+ children: React.ReactNode
49
+ }) => (
50
+ <div data-testid='mock-group-wrapper' data-title={title}>
51
+ {children}
52
+ </div>
53
+ ),
54
+ }))
55
+
56
+ vi.mock('../responsive-drawer/responsive-drawer', () => ({
57
+ ResponsiveDrawer: ({
58
+ children,
59
+ collapsed,
60
+ }: {
61
+ children: React.ReactNode
62
+ collapsed: boolean
63
+ }) => (
64
+ <div data-testid='mock-drawer' data-collapsed={collapsed}>
65
+ {children}
66
+ </div>
67
+ ),
68
+ }))
69
+
70
+ describe('BasemapsUI', () => {
71
+ const mockOptions: BasemapOption[] = [
72
+ { id: 'basemap1', label: 'Basemap 1', icon: 'icon1.png', group: 'Group 1' },
73
+ { id: 'basemap2', label: 'Basemap 2', icon: 'icon2.png', group: 'Group 1' },
74
+ { id: 'basemap3', label: 'Basemap 3', icon: 'icon3.png', group: 'Group 2' },
75
+ ]
76
+
77
+ const mockOnChange = vi.fn()
78
+
79
+ beforeEach(() => {
80
+ mockOnChange.mockReset()
81
+ })
82
+
83
+ test('should render with toggle button initially', () => {
84
+ render(
85
+ <BasemapsUI
86
+ options={mockOptions}
87
+ selected='basemap1'
88
+ onChange={mockOnChange}
89
+ />,
90
+ )
91
+
92
+ // Drawer should be collapsed initially
93
+ const drawer = screen.getByTestId('mock-drawer')
94
+ expect(drawer.getAttribute('data-collapsed')).toBe('true')
95
+
96
+ // Toggle button should be visible
97
+ expect(screen.getAllByRole('button')[0]).toBeDefined()
98
+ })
99
+
100
+ test('should show drawer when toggle button is clicked', () => {
101
+ render(
102
+ <BasemapsUI
103
+ options={mockOptions}
104
+ selected='basemap1'
105
+ onChange={mockOnChange}
106
+ />,
107
+ )
108
+
109
+ // Click toggle button
110
+ const toggleButton = screen.getAllByRole('button')[0]!
111
+ fireEvent.click(toggleButton)
112
+
113
+ // Drawer should not be collapsed after toggle
114
+ const drawer = screen.getByTestId('mock-drawer')
115
+ expect(drawer.getAttribute('data-collapsed')).toBe('false')
116
+ })
117
+
118
+ test('should pass correct props to Group component', () => {
119
+ render(
120
+ <BasemapsUI
121
+ options={mockOptions}
122
+ selected='basemap1'
123
+ onChange={mockOnChange}
124
+ />,
125
+ )
126
+
127
+ // Click toggle button to show drawer
128
+ const toggleButton = screen.getAllByRole('button')[0]!
129
+ fireEvent.click(toggleButton)
130
+
131
+ // Group wrappers should be rendered with correct titles
132
+ const groupWrappers = screen.getAllByTestId('mock-group-wrapper')
133
+ expect(groupWrappers.length).toBe(2) // Two groups
134
+ expect(groupWrappers[0]?.getAttribute('data-title')).toContain('Group 1')
135
+ expect(groupWrappers[1]?.getAttribute('data-title')).toContain('Group 2')
136
+
137
+ // Check basemap options are rendered
138
+ expect(screen.getByTestId('basemap-option-basemap1')).toBeDefined()
139
+ expect(screen.getByTestId('basemap-option-basemap2')).toBeDefined()
140
+ expect(screen.getByTestId('basemap-option-basemap3')).toBeDefined()
141
+ })
142
+
143
+ test('should call onChange when a basemap is selected', () => {
144
+ render(
145
+ <BasemapsUI
146
+ options={mockOptions}
147
+ selected='basemap1'
148
+ onChange={mockOnChange}
149
+ />,
150
+ )
151
+
152
+ // Click toggle button to show drawer
153
+ const toggleButton = screen.getAllByRole('button')[0]!
154
+ fireEvent.click(toggleButton)
155
+
156
+ // Click a basemap option
157
+ const basemapOption = screen.getByTestId('basemap-option-basemap2')
158
+ fireEvent.click(basemapOption)
159
+
160
+ // onChange should be called with correct ID
161
+ expect(mockOnChange).toHaveBeenCalledWith('basemap2')
162
+ })
163
+
164
+ test('should handle small devices correctly using media query', () => {
165
+ // Mock matchMedia for small devices
166
+ window.matchMedia = vi.fn().mockImplementation((query: string) => ({
167
+ matches: query.includes('max-width'),
168
+ media: query,
169
+ onchange: null,
170
+ addListener: vi.fn(),
171
+ removeListener: vi.fn(),
172
+ addEventListener: vi.fn(),
173
+ removeEventListener: vi.fn(),
174
+ dispatchEvent: vi.fn(),
175
+ }))
176
+
177
+ render(
178
+ <BasemapsUI
179
+ options={mockOptions}
180
+ selected='basemap1'
181
+ onChange={mockOnChange}
182
+ position='bottom-right'
183
+ />,
184
+ )
185
+
186
+ // Check if drawer behavior is appropriate for small devices
187
+ const drawer = screen.getByTestId('mock-drawer')
188
+
189
+ // Toggle button should still be visible on small devices
190
+ const toggleButton = screen.getAllByRole('button')[0]
191
+ expect(toggleButton).toBeDefined()
192
+
193
+ // Drawer should be collapsed initially even on small devices
194
+ expect(drawer.getAttribute('data-collapsed')).toBe('true')
195
+ })
196
+ })
@@ -0,0 +1,128 @@
1
+ import { IconButton, Paper, useMediaQuery, useTheme } from '@mui/material'
2
+ import type { BasemapsUIProps } from './types'
3
+ import { useEffect, useRef, useState, type JSX } from 'react'
4
+ import { styles } from './styles'
5
+ import deepmerge from 'deepmerge'
6
+ import { DEFAULT_BASEMAPS_LABELS } from './const'
7
+ import { Header } from './header'
8
+ import { GroupWrapper } from './group-wrapper'
9
+ import { Group } from './group'
10
+ import { ResponsiveDrawer } from '../responsive-drawer/responsive-drawer'
11
+ import { Tooltip } from '../tooltip/tooltip'
12
+
13
+ export function BasemapsUI({
14
+ options = [],
15
+ labels: _labels,
16
+ position = 'bottom-right',
17
+ selected = null,
18
+ onChange,
19
+ TooltipProps,
20
+ }: BasemapsUIProps): JSX.Element {
21
+ const [toggleRef, setToggleRef] = useState<HTMLButtonElement | null>(null)
22
+ const [isOpen, setIsOpen] = useState(false)
23
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null)
24
+ const [timer, setTimer] = useState<number | null>(null)
25
+ const theme = useTheme()
26
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
27
+ const labels = deepmerge(DEFAULT_BASEMAPS_LABELS, _labels ?? {})
28
+
29
+ const optionSelected = options.find((option) => option.id === selected)
30
+
31
+ const groups = Object.groupBy(options, (option) => option.group ?? 'default')
32
+
33
+ const groupCount = Object.keys(groups).length
34
+
35
+ const handleClose = () => {
36
+ setToggleRef(null)
37
+ setIsOpen(false)
38
+ }
39
+
40
+ useEffect(() => {
41
+ if (timeoutRef.current) {
42
+ clearTimeout(timeoutRef.current)
43
+ }
44
+
45
+ if (!timer) {
46
+ return
47
+ }
48
+
49
+ timeoutRef.current = setTimeout(() => {
50
+ handleClose()
51
+ return
52
+ }, timer)
53
+
54
+ return () => {
55
+ if (timeoutRef.current) {
56
+ clearTimeout(timeoutRef.current)
57
+ }
58
+ }
59
+ }, [timer])
60
+
61
+ const handleMouseOver = () => {
62
+ setTimer(null)
63
+ }
64
+ const handleMouseLeave = () => {
65
+ setTimer(400)
66
+ }
67
+
68
+ return (
69
+ <Paper>
70
+ <Tooltip title={labels.toggle.title} placement='right' {...TooltipProps}>
71
+ <IconButton
72
+ sx={styles.toggle}
73
+ onClick={(e) => {
74
+ setToggleRef(e.currentTarget as HTMLButtonElement)
75
+ setIsOpen(true)
76
+ }}
77
+ >
78
+ <img
79
+ src={optionSelected?.icon}
80
+ alt={optionSelected?.label ?? 'Basemap'}
81
+ />
82
+ </IconButton>
83
+ </Tooltip>
84
+
85
+ <ResponsiveDrawer
86
+ ref={toggleRef}
87
+ slotProps={{
88
+ paper: {
89
+ sx: {
90
+ ...styles.root,
91
+ transform: `translate3d(${position.endsWith('right') ? '-8px' : '8px'}, ${
92
+ position.startsWith('bottom') ? '-4px' : '4px'
93
+ }, 0) !important`,
94
+ },
95
+ onMouseOver: handleMouseOver,
96
+ onMouseLeave: handleMouseLeave,
97
+ },
98
+ }}
99
+ collapsed={!isOpen}
100
+ position={position}
101
+ isMobile={isMobile}
102
+ onChangeCollapsed={() => handleClose()}
103
+ >
104
+ <Header
105
+ isMobile={isMobile}
106
+ labels={labels.header}
107
+ onChangeCollapsed={() => handleClose()}
108
+ />
109
+ {Object.entries(groups).map(([group, values]) => (
110
+ <GroupWrapper
111
+ key={group}
112
+ title={group + ' ' + labels.header.title}
113
+ collapsed={groupCount < 2}
114
+ >
115
+ <Group
116
+ values={values!}
117
+ selected={selected}
118
+ onChange={(e) => {
119
+ handleClose()
120
+ onChange(e)
121
+ }}
122
+ />
123
+ </GroupWrapper>
124
+ ))}
125
+ </ResponsiveDrawer>
126
+ </Paper>
127
+ )
128
+ }
@@ -0,0 +1,13 @@
1
+ import type { BasemapsUILabels } from './types'
2
+
3
+ export const DEFAULT_BASEMAPS_LABELS: BasemapsUILabels = {
4
+ toggle: {
5
+ title: 'Toggle basemaps controls',
6
+ },
7
+ header: {
8
+ title: 'Basemaps',
9
+ actions: {
10
+ close: 'Close',
11
+ },
12
+ },
13
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { GroupWrapper } from './group-wrapper'
4
+
5
+ describe('GroupWrapper', () => {
6
+ const mockTitle = 'Group Title'
7
+ const mockChildContent = 'Child Content'
8
+ const mockChild = <div>{mockChildContent}</div>
9
+
10
+ test('should render title and children when not collapsed', () => {
11
+ render(
12
+ <GroupWrapper title={mockTitle} collapsed={false}>
13
+ {mockChild}
14
+ </GroupWrapper>,
15
+ )
16
+
17
+ expect(screen.getByText(mockTitle)).toBeDefined()
18
+ expect(screen.getByText(mockChildContent)).toBeDefined()
19
+ })
20
+
21
+ test('should only render children when collapsed', () => {
22
+ render(
23
+ <GroupWrapper title={mockTitle} collapsed={true}>
24
+ {mockChild}
25
+ </GroupWrapper>,
26
+ )
27
+
28
+ expect(screen.queryByText(mockTitle)).toBeNull()
29
+ expect(screen.getByText(mockChildContent)).toBeDefined()
30
+ })
31
+
32
+ test('should default to not collapsed when collapsed prop is not provided', () => {
33
+ render(<GroupWrapper title={mockTitle}>{mockChild}</GroupWrapper>)
34
+
35
+ expect(screen.getByText(mockTitle)).toBeDefined()
36
+ expect(screen.getByText(mockChildContent)).toBeDefined()
37
+ })
38
+ })
@@ -0,0 +1,28 @@
1
+ import { Typography } from '@mui/material'
2
+ import type { ReactNode } from 'react'
3
+ import { styles } from './styles'
4
+
5
+ export function GroupWrapper({
6
+ title,
7
+ children,
8
+ collapsed = false,
9
+ }: {
10
+ title: string
11
+ children: ReactNode
12
+ collapsed?: boolean
13
+ }) {
14
+ if (collapsed) return children
15
+
16
+ return (
17
+ <>
18
+ <Typography
19
+ variant='caption'
20
+ color='textSecondary'
21
+ sx={styles.groupWrapper.content}
22
+ >
23
+ {title}
24
+ </Typography>
25
+ {children}
26
+ </>
27
+ )
28
+ }
@@ -0,0 +1,52 @@
1
+ import { describe, test, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { Group } from './group'
4
+
5
+ describe('Group', () => {
6
+ const mockOptions = [
7
+ { id: 'basemap1', label: 'Basemap 1', icon: 'icon1.png' },
8
+ { id: 'basemap2', label: 'Basemap 2', icon: 'icon2.png' },
9
+ ]
10
+
11
+ const mockOnChange = vi.fn()
12
+
13
+ test('should render all provided basemap options', () => {
14
+ render(
15
+ <Group values={mockOptions} selected={null} onChange={mockOnChange} />,
16
+ )
17
+
18
+ mockOptions.forEach((option) => {
19
+ expect(screen.getByText(option.label)).toBeDefined()
20
+ expect(screen.getByLabelText(option.label)).toBeDefined() // Button has aria-label equal to option.label
21
+ expect(screen.getByLabelText(option.label + ' icon')).toBeDefined() // Image has aria-label equal to option.label + ' icon'
22
+ })
23
+ })
24
+
25
+ test('should mark selected option correctly', () => {
26
+ render(
27
+ <Group
28
+ values={mockOptions}
29
+ selected='basemap1'
30
+ onChange={mockOnChange}
31
+ />,
32
+ )
33
+
34
+ const selectedImg = screen.getByLabelText('Basemap 1 icon') // The img element
35
+ expect(selectedImg.getAttribute('data-active')).toBe('true')
36
+
37
+ const nonSelectedImg = screen.getByLabelText('Basemap 2 icon') // The img element
38
+ expect(nonSelectedImg.getAttribute('data-active')).toBe('false')
39
+ })
40
+
41
+ test('should call onChange with correct ID when an option is clicked', () => {
42
+ render(
43
+ <Group values={mockOptions} selected={null} onChange={mockOnChange} />,
44
+ )
45
+
46
+ const button = screen.getByLabelText('Basemap 2')
47
+ fireEvent.click(button)
48
+
49
+ expect(mockOnChange).toHaveBeenCalledTimes(1)
50
+ expect(mockOnChange).toHaveBeenCalledWith('basemap2')
51
+ })
52
+ })
@@ -0,0 +1,42 @@
1
+ import { Box, Typography } from '@mui/material'
2
+ import { styles } from './styles'
3
+ import type { BasemapOption } from './types'
4
+
5
+ export function Group({
6
+ values,
7
+ selected,
8
+ onChange,
9
+ }: {
10
+ values: Omit<BasemapOption, 'group'>[]
11
+ selected: string | null | undefined
12
+ onChange: (id: string) => void
13
+ }) {
14
+ return (
15
+ <Box sx={styles.group}>
16
+ {values.map((option) => {
17
+ const isSelected = option.id === selected
18
+ return (
19
+ <Box
20
+ key={option.id}
21
+ sx={styles.item.container}
22
+ component='button'
23
+ aria-label={option.label}
24
+ onClick={() => onChange(option.id)}
25
+ >
26
+ <img
27
+ src={option.icon}
28
+ aria-label={option.label + ' icon'}
29
+ data-active={isSelected}
30
+ />
31
+ <Typography
32
+ variant='caption'
33
+ sx={{ ...(isSelected && styles.item.selected) }}
34
+ >
35
+ {option.label}
36
+ </Typography>
37
+ </Box>
38
+ )
39
+ })}
40
+ </Box>
41
+ )
42
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, test, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { Header } from './header'
4
+
5
+ describe('Header', () => {
6
+ const mockLabels = {
7
+ title: 'Basemap Selection',
8
+ actions: {
9
+ close: 'Close Panel',
10
+ },
11
+ }
12
+
13
+ const mockOnChangeCollapsed = vi.fn()
14
+
15
+ test('should not render title and close button when not mobile', () => {
16
+ render(
17
+ <Header
18
+ isMobile={false}
19
+ labels={mockLabels}
20
+ onChangeCollapsed={mockOnChangeCollapsed}
21
+ />,
22
+ )
23
+
24
+ expect(screen.queryByText(mockLabels.title)).toBeNull()
25
+ expect(screen.queryByLabelText(mockLabels.actions.close)).toBeNull()
26
+ })
27
+
28
+ test('should render title and close button when mobile', () => {
29
+ render(
30
+ <Header
31
+ isMobile={true}
32
+ labels={mockLabels}
33
+ onChangeCollapsed={mockOnChangeCollapsed}
34
+ />,
35
+ )
36
+
37
+ expect(screen.getByText(mockLabels.title)).toBeDefined()
38
+ expect(screen.getByLabelText(mockLabels.actions.close)).toBeDefined()
39
+ })
40
+
41
+ test('should call onChangeCollapsed when close button is clicked', () => {
42
+ render(
43
+ <Header
44
+ isMobile={true}
45
+ labels={mockLabels}
46
+ onChangeCollapsed={mockOnChangeCollapsed}
47
+ />,
48
+ )
49
+
50
+ const closeButton = screen.getByLabelText(mockLabels.actions.close)
51
+ fireEvent.click(closeButton)
52
+ expect(mockOnChangeCollapsed).toHaveBeenCalledTimes(1)
53
+ })
54
+ })
@@ -0,0 +1,36 @@
1
+ import { Close } from '@mui/icons-material'
2
+ import { Box, Typography, IconButton } from '@mui/material'
3
+ import { styles } from './styles'
4
+ import type { BasemapsUILabels } from './types'
5
+ import { Tooltip } from '../tooltip/tooltip'
6
+
7
+ export function Header({
8
+ isMobile = false,
9
+ labels,
10
+ onChangeCollapsed,
11
+ }: {
12
+ labels: BasemapsUILabels['header']
13
+ isMobile?: boolean
14
+ onChangeCollapsed: () => void
15
+ }) {
16
+ return (
17
+ <Box sx={styles.header}>
18
+ {isMobile && (
19
+ <>
20
+ <Typography variant='caption' flexGrow={1}>
21
+ {labels.title}
22
+ </Typography>
23
+ <Tooltip title={labels.actions.close}>
24
+ <IconButton
25
+ size='small'
26
+ onClick={onChangeCollapsed}
27
+ aria-label={labels.actions.close}
28
+ >
29
+ <Close />
30
+ </IconButton>
31
+ </Tooltip>
32
+ </>
33
+ )}
34
+ </Box>
35
+ )
36
+ }