@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,165 @@
1
+ import { describe, test, vi, expect, beforeEach } from 'vitest'
2
+ import { render, screen, within } from '@testing-library/react'
3
+ import userEvent from '@testing-library/user-event'
4
+ import { MeasurementToolsUI } from './measurement-tools'
5
+ import type { MeasurementToolsComponentProps } from './types'
6
+ import type { MeasurementMode } from '../types'
7
+ import {
8
+ changeMeasurementModeOptions,
9
+ DEFAULT_MEASUREMENT_TOOLS_MODES,
10
+ DEFAULT_MEASUREMENT_TOOLS_UNITS,
11
+ } from '../../../tests/config'
12
+
13
+ describe('MeasurementToolsUI', () => {
14
+ const mockOnActionToggle = vi.fn()
15
+ const mockOnChangeMode = vi.fn()
16
+ const mockOnChangeUnit = vi.fn()
17
+
18
+ beforeEach(() => {
19
+ vi.clearAllMocks()
20
+ })
21
+
22
+ test('should enable measurement', async () => {
23
+ const { container } = render(
24
+ <Component
25
+ enabled={false}
26
+ modes={DEFAULT_MEASUREMENT_TOOLS_MODES}
27
+ modeSelected={DEFAULT_MEASUREMENT_TOOLS_MODES.distance.value}
28
+ units={DEFAULT_MEASUREMENT_TOOLS_UNITS}
29
+ mockOnActionToggle={mockOnActionToggle}
30
+ mockOnChangeMode={mockOnChangeMode}
31
+ mockOnChangeUnit={mockOnChangeUnit}
32
+ />,
33
+ )
34
+
35
+ const rulerButton = within(container).getByLabelText(
36
+ 'Click on the map to start measuring',
37
+ )
38
+ await userEvent.click(rulerButton)
39
+
40
+ expect(mockOnActionToggle).toHaveBeenCalledWith(true)
41
+ })
42
+
43
+ for (const [mode, label] of Object.entries(changeMeasurementModeOptions)) {
44
+ test(`should change mode to ${label}`, async () => {
45
+ const { container } = render(
46
+ <Component
47
+ enabled
48
+ modes={DEFAULT_MEASUREMENT_TOOLS_MODES}
49
+ modeSelected={mode as MeasurementMode}
50
+ units={DEFAULT_MEASUREMENT_TOOLS_UNITS}
51
+ mockOnActionToggle={mockOnActionToggle}
52
+ mockOnChangeMode={mockOnChangeMode}
53
+ mockOnChangeUnit={mockOnChangeUnit}
54
+ />,
55
+ )
56
+
57
+ const dropdownButton = within(container).getAllByRole('button')[1]!
58
+ await userEvent.click(dropdownButton)
59
+
60
+ const modeButton = await screen.findByRole('menuitem', {
61
+ name: label,
62
+ })
63
+ await userEvent.click(modeButton)
64
+
65
+ expect(mockOnChangeMode).toHaveBeenCalledWith(mode)
66
+ })
67
+ }
68
+
69
+ test('should change meters and kilometers units', async () => {
70
+ const { container } = render(
71
+ <Component
72
+ enabled
73
+ modes={DEFAULT_MEASUREMENT_TOOLS_MODES}
74
+ modeSelected={DEFAULT_MEASUREMENT_TOOLS_MODES.distance.value}
75
+ units={DEFAULT_MEASUREMENT_TOOLS_UNITS}
76
+ mockOnActionToggle={mockOnActionToggle}
77
+ mockOnChangeMode={mockOnChangeMode}
78
+ mockOnChangeUnit={mockOnChangeUnit}
79
+ />,
80
+ )
81
+
82
+ const dropdownButton = within(container).getAllByRole('button')[1]!
83
+ await userEvent.click(dropdownButton)
84
+
85
+ const unitButton = await screen.findByText('Unit of measurement')
86
+ await userEvent.click(unitButton)
87
+
88
+ const dialog = await screen.findByRole('dialog')
89
+
90
+ const radioGroup = within(dialog).getByRole('radiogroup')
91
+ const kilometerRadioButton = within(radioGroup).getByText('Kilometers')
92
+ await userEvent.click(kilometerRadioButton)
93
+
94
+ const saveButton = within(dialog).getByText('Save')
95
+ await userEvent.click(saveButton)
96
+ expect(mockOnChangeUnit).toHaveBeenCalledWith('kilometer')
97
+ })
98
+
99
+ test('should change feet and miles units', async () => {
100
+ const { container } = render(
101
+ <Component
102
+ enabled
103
+ modes={DEFAULT_MEASUREMENT_TOOLS_MODES}
104
+ modeSelected={DEFAULT_MEASUREMENT_TOOLS_MODES.distance.value}
105
+ units={DEFAULT_MEASUREMENT_TOOLS_UNITS}
106
+ mockOnActionToggle={mockOnActionToggle}
107
+ mockOnChangeMode={mockOnChangeMode}
108
+ mockOnChangeUnit={mockOnChangeUnit}
109
+ />,
110
+ )
111
+
112
+ const dropdownButton = within(container).getAllByRole('button')[1]!
113
+ await userEvent.click(dropdownButton)
114
+
115
+ const unitButton = await screen.findByText('Unit of measurement')
116
+ await userEvent.click(unitButton)
117
+
118
+ const dialog = await screen.findByRole('dialog')
119
+
120
+ const feetAndMilesButton = within(dialog).getByText('Feet and Miles')
121
+ await userEvent.click(feetAndMilesButton)
122
+
123
+ const radioGroup = await within(dialog).findByRole('radiogroup')
124
+ const mileRadioButton = await within(radioGroup).findByText('Mile')
125
+ await userEvent.click(mileRadioButton)
126
+
127
+ const saveButton = within(dialog).getByText('Save')
128
+ await userEvent.click(saveButton)
129
+ expect(mockOnChangeUnit).toHaveBeenCalledWith('mile')
130
+ })
131
+ })
132
+
133
+ interface ComponentProps {
134
+ enabled: boolean
135
+ modes: MeasurementToolsComponentProps['modes']
136
+ modeSelected: MeasurementToolsComponentProps['modeSelected']
137
+ units: MeasurementToolsComponentProps['units']
138
+ mockOnActionToggle: MeasurementToolsComponentProps['onActionToggle']
139
+ mockOnChangeMode: MeasurementToolsComponentProps['onChangeMode']
140
+ mockOnChangeUnit: MeasurementToolsComponentProps['onChangeUnit']
141
+ }
142
+
143
+ // Aux
144
+ const Component = ({
145
+ enabled,
146
+ modes,
147
+ modeSelected,
148
+ units,
149
+ mockOnActionToggle,
150
+ mockOnChangeMode,
151
+ mockOnChangeUnit,
152
+ }: ComponentProps) => {
153
+ return (
154
+ <MeasurementToolsUI
155
+ value='1000m'
156
+ enabled={enabled}
157
+ modes={modes}
158
+ modeSelected={modeSelected}
159
+ units={units}
160
+ onActionToggle={mockOnActionToggle}
161
+ onChangeMode={mockOnChangeMode}
162
+ onChangeUnit={mockOnChangeUnit}
163
+ />
164
+ )
165
+ }
@@ -0,0 +1,443 @@
1
+ import { ArrowDropDown, Close } from '@mui/icons-material'
2
+ import {
3
+ Box,
4
+ Button,
5
+ Dialog,
6
+ DialogActions,
7
+ DialogContent,
8
+ DialogTitle,
9
+ Divider,
10
+ FormControl,
11
+ FormControlLabel,
12
+ IconButton,
13
+ ListItemIcon,
14
+ ListItemText,
15
+ Menu,
16
+ MenuItem,
17
+ Paper,
18
+ Radio,
19
+ RadioGroup,
20
+ SvgIcon,
21
+ ToggleButton,
22
+ ToggleButtonGroup,
23
+ Typography,
24
+ } from '@mui/material'
25
+ import {
26
+ useMemo,
27
+ useState,
28
+ type FormEvent,
29
+ type JSX,
30
+ type MouseEvent,
31
+ type PropsWithChildren,
32
+ } from 'react'
33
+ import { styles } from './styles'
34
+ import deepmerge from 'deepmerge'
35
+ import type { MeasurementToolsComponentProps } from './types'
36
+ import type { PickDeep, ValueOf } from 'type-fest'
37
+ import {
38
+ DEFAULT_MEASUREMENT_TOOLS_LABELS,
39
+ DEFAULT_MEASUREMENT_TOOLS_UNITS_MAPPING,
40
+ DEFAULT_MEASUREMENT_TOOLS_MODES_MAPPING,
41
+ } from './const'
42
+ import type {
43
+ MeasurementSystem,
44
+ MeasurementUnit,
45
+ MeasurementUnitImperialDistance,
46
+ MeasurementUnitMetricDistance,
47
+ MeasurementUnitOption,
48
+ MeasureUnitMapping,
49
+ } from '../types'
50
+ import { Tooltip } from '../tooltip/tooltip'
51
+
52
+ export function MeasurementToolsUI({
53
+ enabled,
54
+ actionProps,
55
+ labels = DEFAULT_MEASUREMENT_TOOLS_LABELS,
56
+ modes,
57
+ modesMapping = DEFAULT_MEASUREMENT_TOOLS_MODES_MAPPING,
58
+ unitsMapping = DEFAULT_MEASUREMENT_TOOLS_UNITS_MAPPING,
59
+ modeSelected,
60
+ PaperProps: { sx, ...PaperProps } = {},
61
+ units,
62
+ unitSelected,
63
+ onActionToggle,
64
+ onChangeMode,
65
+ onChangeUnit,
66
+ }: MeasurementToolsComponentProps): JSX.Element {
67
+ const handleToggle: MeasurementToolsComponentProps['onActionToggle'] = (
68
+ data,
69
+ ) => {
70
+ return onActionToggle(!!data)
71
+ }
72
+
73
+ const data = useMemo(() => {
74
+ return deepmerge(modes, modesMapping)
75
+ }, [modes, modesMapping])
76
+
77
+ const modeExists = (modeSelected ?? '') in data
78
+
79
+ const mode = (
80
+ modeExists ? modeSelected : Object.keys(data)[0]
81
+ ) as keyof typeof modes
82
+
83
+ const modeSelectedValue = data[mode]
84
+
85
+ const _units = useMemo(() => {
86
+ const option = units[mode]
87
+ const options =
88
+ Object.keys(units).length >= 1 ? units : { [mode]: option.slice(0, 1) }
89
+ return options[mode].map((unit) => {
90
+ return {
91
+ ...unit,
92
+ ...unitsMapping[unit.value],
93
+ }
94
+ })
95
+ }, [mode, units, unitsMapping])
96
+
97
+ return (
98
+ <Paper
99
+ sx={{
100
+ ...styles.container,
101
+ ...sx,
102
+ }}
103
+ {...PaperProps}
104
+ >
105
+ <Box>
106
+ <MeasurementToolsUIAction
107
+ actionProps={actionProps}
108
+ labels={labels?.action}
109
+ enabled={enabled}
110
+ onActionToggle={handleToggle}
111
+ >
112
+ {modeSelectedValue?.icon}
113
+ </MeasurementToolsUIAction>
114
+ <Options
115
+ labels={labels?.options}
116
+ modes={data}
117
+ modeSelected={mode}
118
+ units={_units}
119
+ unitSelected={unitSelected}
120
+ onChangeMode={onChangeMode}
121
+ onChangeUnit={onChangeUnit}
122
+ />
123
+ </Box>
124
+ </Paper>
125
+ )
126
+ }
127
+
128
+ function MeasurementToolsUIAction({
129
+ actionProps,
130
+ labels,
131
+ enabled,
132
+ children,
133
+ onActionToggle,
134
+ }: PropsWithChildren<
135
+ Pick<
136
+ MeasurementToolsComponentProps,
137
+ 'actionProps' | 'enabled' | 'onActionToggle'
138
+ > & {
139
+ labels?: PickDeep<
140
+ MeasurementToolsComponentProps['labels'],
141
+ 'action'
142
+ >['action']
143
+ }
144
+ >) {
145
+ const actionState = enabled ? 'active' : 'inactive'
146
+ const actionLabel =
147
+ labels?.tooltip?.[actionState] ??
148
+ DEFAULT_MEASUREMENT_TOOLS_LABELS.action.tooltip[actionState]
149
+
150
+ return (
151
+ <Tooltip
152
+ title={actionLabel}
153
+ placement='right'
154
+ {...actionProps?.TooltipProps}
155
+ >
156
+ <ToggleButton
157
+ value='toggle'
158
+ sx={styles.actions.icon}
159
+ onClick={() => onActionToggle(!enabled)}
160
+ aria-label={actionLabel}
161
+ selected={enabled}
162
+ >
163
+ {children}
164
+ </ToggleButton>
165
+ </Tooltip>
166
+ )
167
+ }
168
+
169
+ function Options({
170
+ modes,
171
+ modeSelected,
172
+ units,
173
+ unitSelected,
174
+ labels,
175
+ onChangeMode,
176
+ onChangeUnit,
177
+ }: Required<
178
+ Pick<
179
+ MeasurementToolsComponentProps,
180
+ 'modeSelected' | 'onChangeMode' | 'onChangeUnit'
181
+ >
182
+ > & {
183
+ modes: MeasurementToolsComponentProps['modes'] &
184
+ MeasurementToolsComponentProps['modesMapping']
185
+ units: (MeasurementUnitOption<MeasurementUnit> & MeasureUnitMapping)[]
186
+ unitSelected?: MeasurementToolsComponentProps['unitSelected']
187
+ labels?: PickDeep<
188
+ MeasurementToolsComponentProps['labels'],
189
+ 'options'
190
+ >['options']
191
+ }) {
192
+ const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
193
+ const [unitModal, setUnitModal] = useState(false)
194
+
195
+ const modesAvailable = Object.values(modes)
196
+ const unitsAvailable = Object.values(units)
197
+
198
+ const hasValues = modesAvailable.length > 1 || unitsAvailable.length > 1
199
+
200
+ if (!hasValues) {
201
+ return null
202
+ }
203
+
204
+ const open = Boolean(anchorEl)
205
+
206
+ const handleToggle = (event: MouseEvent<HTMLButtonElement>) => {
207
+ setAnchorEl(event.currentTarget)
208
+ }
209
+
210
+ const handleClose = () => {
211
+ setAnchorEl(null)
212
+ }
213
+
214
+ const handleClick = (
215
+ e: MouseEvent<HTMLLIElement>,
216
+ mode: ValueOf<Required<typeof modes>>,
217
+ ) => {
218
+ e.preventDefault()
219
+ onChangeMode(mode.value)
220
+ handleClose()
221
+ }
222
+
223
+ const handleClickUnit = () => {
224
+ setUnitModal(true)
225
+ }
226
+
227
+ const handleCloseModal = () => {
228
+ setUnitModal(false)
229
+ }
230
+
231
+ const handleApplyModal = (data: string) => {
232
+ handleCloseModal()
233
+ onChangeUnit(data)
234
+ }
235
+
236
+ const modeTitle =
237
+ labels?.mode?.title ?? DEFAULT_MEASUREMENT_TOOLS_LABELS.options.mode.title
238
+
239
+ const unitTitle =
240
+ labels?.units?.title ?? DEFAULT_MEASUREMENT_TOOLS_LABELS.options.units.title
241
+
242
+ const hasDivider = modesAvailable.length > 1 && unitsAvailable.length > 1
243
+
244
+ const unitSelectedValue = (units.find(
245
+ (unit) => unit.value === unitSelected,
246
+ ) ?? units[0])!
247
+
248
+ return (
249
+ <>
250
+ <IconButton sx={styles.options.icon} onClick={handleToggle}>
251
+ <ArrowDropDown />
252
+ </IconButton>
253
+ <Menu
254
+ id='measurement-menu'
255
+ anchorEl={anchorEl}
256
+ open={open}
257
+ onClose={handleClose}
258
+ MenuListProps={{
259
+ 'aria-labelledby': 'basic-button',
260
+ sx: styles.options.menu,
261
+ }}
262
+ >
263
+ <div>
264
+ {modesAvailable.length > 1 && (
265
+ <>
266
+ <Typography
267
+ variant='subtitle2'
268
+ color='text.secondary'
269
+ sx={styles.options.title}
270
+ >
271
+ {modeTitle}
272
+ </Typography>
273
+ {modesAvailable.map((mode) => {
274
+ const options =
275
+ labels?.mode?.options ??
276
+ DEFAULT_MEASUREMENT_TOOLS_LABELS.options.mode.options
277
+ const label = options[mode.value]
278
+
279
+ return (
280
+ <MenuItem
281
+ key={mode.value}
282
+ onClick={(e) => handleClick(e, mode)}
283
+ selected={mode.value === modeSelected}
284
+ >
285
+ <ListItemIcon sx={styles.options.icons}>
286
+ <SvgIcon>{mode.icon}</SvgIcon>
287
+ </ListItemIcon>
288
+ <ListItemText>{label}</ListItemText>
289
+ </MenuItem>
290
+ )
291
+ })}
292
+ </>
293
+ )}
294
+ {hasDivider && <Divider />}
295
+ {unitsAvailable.length > 1 && (
296
+ <>
297
+ <MenuItem onClick={handleClickUnit}>
298
+ <ListItemText>{unitTitle}</ListItemText>
299
+ <Typography
300
+ variant='caption'
301
+ fontWeight={500}
302
+ sx={styles.options.tag}
303
+ >
304
+ {unitSelectedValue.short}
305
+ </Typography>
306
+ </MenuItem>
307
+ <UnitsModal
308
+ open={unitModal}
309
+ labels={labels}
310
+ units={units}
311
+ unitSelectedValue={unitSelectedValue}
312
+ onClose={handleCloseModal}
313
+ onSubmit={handleApplyModal}
314
+ />
315
+ </>
316
+ )}
317
+ </div>
318
+ </Menu>
319
+ </>
320
+ )
321
+ }
322
+
323
+ function UnitsModal({
324
+ open,
325
+ labels,
326
+ units,
327
+ unitSelectedValue,
328
+ onClose,
329
+ onSubmit,
330
+ }: {
331
+ open: boolean
332
+ labels?: PickDeep<
333
+ MeasurementToolsComponentProps['labels'],
334
+ 'options'
335
+ >['options']
336
+ units: MeasurementUnitOption<MeasurementUnit>[]
337
+ unitSelectedValue: MeasurementUnitOption<MeasurementUnit>
338
+ onClose: () => void
339
+ onSubmit: (data: string) => void
340
+ }) {
341
+ const [system, setSystem] = useState<MeasurementSystem>('metric')
342
+
343
+ const _units = useMemo(() => {
344
+ return units.filter((unit) => unit.system === system)
345
+ }, [system, units])
346
+
347
+ const handleToggleChange = (
348
+ _: MouseEvent<HTMLElement>,
349
+ value: MeasurementSystem,
350
+ ) => {
351
+ setSystem(value)
352
+ }
353
+
354
+ const _labels =
355
+ labels?.units?.modal?.options ??
356
+ DEFAULT_MEASUREMENT_TOOLS_LABELS.options.units.modal.options
357
+
358
+ const labelsBySystem = _labels[system].options
359
+
360
+ const modalUnitTitle =
361
+ labels?.units?.modal?.title ??
362
+ DEFAULT_MEASUREMENT_TOOLS_LABELS.options.units.modal.title
363
+ const modalUnitSubtitle =
364
+ labels?.units?.modal?.subtitle ??
365
+ DEFAULT_MEASUREMENT_TOOLS_LABELS.options.units.modal.subtitle
366
+ const modalUnitApply =
367
+ labels?.units?.modal?.apply ??
368
+ DEFAULT_MEASUREMENT_TOOLS_LABELS.options.units.modal.apply
369
+
370
+ const _unitSelectedValue =
371
+ unitSelectedValue.system === system ? unitSelectedValue : _units[0]
372
+
373
+ return (
374
+ <Dialog
375
+ open={open}
376
+ component={Box}
377
+ onClose={onClose}
378
+ maxWidth='xs'
379
+ PaperProps={{
380
+ component: 'form',
381
+ onSubmit: (e: FormEvent<HTMLFormElement>) => {
382
+ e.preventDefault()
383
+ const formData = new FormData(e.currentTarget)
384
+ onSubmit(formData.get('unit-value') as string)
385
+ },
386
+ }}
387
+ >
388
+ <DialogTitle sx={styles.options.modal.title}>
389
+ {modalUnitTitle}
390
+ <IconButton onClick={onClose}>
391
+ <Close />
392
+ </IconButton>
393
+ </DialogTitle>
394
+ <DialogContent sx={styles.options.modal.content}>
395
+ <Typography sx={styles.options.modal.subtitle}>
396
+ {modalUnitSubtitle}
397
+ </Typography>
398
+ <ToggleButtonGroup
399
+ color='primary'
400
+ value={system}
401
+ exclusive={true}
402
+ fullWidth={true}
403
+ onChange={handleToggleChange}
404
+ aria-label='system'
405
+ sx={styles.options.modal.toggle}
406
+ >
407
+ <ToggleButton value='metric'>{_labels.metric.title}</ToggleButton>
408
+ <ToggleButton value='imperial'>{_labels.imperial.title}</ToggleButton>
409
+ </ToggleButtonGroup>
410
+ <FormControl key={_unitSelectedValue?.value}>
411
+ <RadioGroup
412
+ aria-labelledby='unit-value'
413
+ defaultValue={_unitSelectedValue?.value}
414
+ name='unit-value'
415
+ sx={styles.options.modal.optionsGroup}
416
+ >
417
+ {_units.map((unit) => {
418
+ const label =
419
+ labelsBySystem[
420
+ unit.value as MeasurementUnitMetricDistance &
421
+ MeasurementUnitImperialDistance
422
+ ]
423
+
424
+ return (
425
+ <FormControlLabel
426
+ key={unit.value}
427
+ value={unit.value}
428
+ control={<Radio />}
429
+ label={label}
430
+ />
431
+ )
432
+ })}
433
+ </RadioGroup>
434
+ </FormControl>
435
+ </DialogContent>
436
+ <DialogActions sx={styles.options.modal.actions}>
437
+ <Button variant='contained' type='submit'>
438
+ {modalUnitApply}
439
+ </Button>
440
+ </DialogActions>
441
+ </Dialog>
442
+ )
443
+ }
@@ -0,0 +1,91 @@
1
+ import { alpha, type SxProps, type Theme } from '@mui/material'
2
+
3
+ export const styles = {
4
+ container: {
5
+ display: 'flex',
6
+ flexDirection: 'row',
7
+ alignItems: 'center',
8
+ justifyContent: 'flex-start',
9
+ gap: ({ spacing }) => spacing(1),
10
+ overflow: 'hidden',
11
+ },
12
+ actions: {
13
+ icon: {
14
+ width: ({ spacing }) => spacing(4),
15
+ height: ({ spacing }) => spacing(4),
16
+ borderRadius: 0,
17
+ '.MuiTouchRipple-ripple .MuiTouchRipple-child': {
18
+ borderRadius: 0,
19
+ },
20
+ },
21
+ },
22
+ options: {
23
+ menu: {
24
+ marginTop: ({ spacing }) => spacing(1),
25
+ },
26
+ icon: {
27
+ width: ({ spacing }) => spacing(3),
28
+ },
29
+ title: {
30
+ paddingX: ({ spacing }) => spacing(2),
31
+ paddingY: ({ spacing }) => spacing(0.5),
32
+ },
33
+ icons: {
34
+ color: ({ palette }) => palette.text.primary,
35
+ },
36
+ tag: {
37
+ borderRadius: ({ spacing }) => spacing(0.25),
38
+ border: '1px solid',
39
+ borderColor: ({ palette }) => palette.primary.main,
40
+ paddingX: ({ spacing }) => spacing(0.5),
41
+ color: ({ palette }) => palette.primary.main,
42
+ backgroundColor: ({ palette }) => alpha(palette.primary.main, 0.08),
43
+ },
44
+ modal: {
45
+ toggle: {
46
+ boxShadow: 'none',
47
+ backgroundColor: '#F8F9F9',
48
+ padding: ({ spacing }) => spacing(0.5),
49
+ marginTop: ({ spacing }) => spacing(2),
50
+ marginBottom: ({ spacing }) => spacing(1),
51
+ '.MuiToggleButtonGroup-grouped': {
52
+ typography: 'caption',
53
+ fontWeight: 500,
54
+ margin: 0,
55
+ '&:first-of-type:not(.MuiDivider-root)': {
56
+ marginLeft: 0,
57
+ },
58
+ },
59
+ },
60
+ subtitle: {
61
+ typography: 'body2',
62
+ color: ({ palette }) => palette.text.secondary,
63
+ },
64
+ optionsGroup: {
65
+ marginLeft: ({ spacing }) => spacing(1),
66
+ typography: 'body2',
67
+ },
68
+ title: {
69
+ display: 'flex',
70
+ justifyContent: 'space-between',
71
+ alignItems: 'center',
72
+ padding: ({ spacing }) => spacing(2),
73
+ },
74
+ content: {
75
+ paddingX: ({ spacing }) => spacing(2),
76
+ minWidth: ({ spacing }) => spacing(50),
77
+ },
78
+ actions: {
79
+ paddingX: ({ spacing }) => spacing(2),
80
+ },
81
+ },
82
+ },
83
+ chip: {
84
+ chip: {
85
+ marginRight: ({ spacing }) => spacing(0.5),
86
+ },
87
+ disabled: {
88
+ opacity: ({ palette }) => palette.action.disabledOpacity,
89
+ },
90
+ },
91
+ } satisfies Record<string, SxProps<Theme>>