@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,354 @@
1
+ import { describe, test, expect, beforeEach, vi, afterEach } from 'vitest'
2
+ import { render, screen, fireEvent, act } from '@testing-library/react'
3
+ import { SearcherToggle } from './searcher-toggle'
4
+ import { Searcher } from './searcher'
5
+ import { useWidgetStore } from '../../stores/widget-store'
6
+ import type { EchartWidgetData } from '../../echart/types'
7
+ import type { SearcherState } from './types'
8
+
9
+ // Test data
10
+ const mockData: EchartWidgetData = [
11
+ [
12
+ { name: 'Electronics', value: 440 },
13
+ { name: 'Clothing', value: 280 },
14
+ { name: 'Books', value: 100 },
15
+ ],
16
+ ]
17
+
18
+ describe('SearcherToggle', () => {
19
+ const widgetId = 'test-searcher-toggle'
20
+
21
+ beforeEach(() => {
22
+ useWidgetStore.getState().clearWidgets()
23
+ })
24
+
25
+ test('renders search toggle button', () => {
26
+ render(<SearcherToggle id={widgetId} />)
27
+
28
+ const button = screen.getByRole('button')
29
+ expect(button).toBeTruthy()
30
+ })
31
+
32
+ test('shows enable tooltip when search is disabled', () => {
33
+ render(<SearcherToggle id={widgetId} />)
34
+
35
+ const button = screen.getByRole('button', { name: 'Enable search' })
36
+ expect(button).toBeTruthy()
37
+ })
38
+
39
+ test('shows disable tooltip when search is enabled', () => {
40
+ render(<SearcherToggle id={widgetId} defaultEnabled />)
41
+
42
+ const button = screen.getByRole('button', { name: 'Disable search' })
43
+ expect(button).toBeTruthy()
44
+ })
45
+
46
+ test('toggles isSearchEnabled in store when clicked', () => {
47
+ render(<SearcherToggle id={widgetId} />)
48
+
49
+ const button = screen.getByRole('button')
50
+ fireEvent.click(button)
51
+
52
+ const widget = useWidgetStore.getState().getWidget<SearcherState>(widgetId)
53
+ expect(widget?.isSearchEnabled).toBe(true)
54
+ })
55
+
56
+ test('has active state when enabled via store', () => {
57
+ render(<SearcherToggle id={widgetId} defaultEnabled />)
58
+
59
+ const button = screen.getByRole('button')
60
+ expect(button.getAttribute('data-active')).toBe('true')
61
+ })
62
+
63
+ test('has inactive state when disabled', () => {
64
+ render(<SearcherToggle id={widgetId} />)
65
+
66
+ const button = screen.getByRole('button')
67
+ expect(button.getAttribute('data-active')).toBe('false')
68
+ })
69
+
70
+ test('uses custom labels', () => {
71
+ render(
72
+ <SearcherToggle
73
+ id={widgetId}
74
+ labels={{
75
+ enable: 'Activar búsqueda',
76
+ disable: 'Desactivar búsqueda',
77
+ ariaLabel: 'Toggle search',
78
+ }}
79
+ />,
80
+ )
81
+
82
+ const button = screen.getByRole('button', { name: 'Toggle search' })
83
+ expect(button).toBeTruthy()
84
+ })
85
+
86
+ test('initializes store with defaultEnabled value', () => {
87
+ render(<SearcherToggle id={widgetId} defaultEnabled />)
88
+
89
+ const widget = useWidgetStore.getState().getWidget<SearcherState>(widgetId)
90
+ expect(widget?.isSearchEnabled).toBe(true)
91
+ })
92
+ })
93
+
94
+ describe('Searcher', () => {
95
+ const widgetId = 'test-searcher-input'
96
+
97
+ beforeEach(() => {
98
+ useWidgetStore.getState().clearWidgets()
99
+ vi.useFakeTimers()
100
+ })
101
+
102
+ afterEach(() => {
103
+ vi.useRealTimers()
104
+ })
105
+
106
+ test('does not render when search is disabled in store', () => {
107
+ // Set isSearchEnabled to false in store
108
+ useWidgetStore.getState().setWidget(widgetId, { isSearchEnabled: false })
109
+
110
+ render(<Searcher id={widgetId} />)
111
+
112
+ const input = screen.queryByRole('textbox')
113
+ expect(input).toBeNull()
114
+ })
115
+
116
+ test('renders input when search is enabled in store', () => {
117
+ // Set isSearchEnabled to true in store
118
+ useWidgetStore.getState().setWidget(widgetId, { isSearchEnabled: true })
119
+
120
+ render(<Searcher id={widgetId} />)
121
+
122
+ const input = screen.getByRole('textbox')
123
+ expect(input).toBeTruthy()
124
+ })
125
+
126
+ test('shows placeholder text', () => {
127
+ useWidgetStore.getState().setWidget(widgetId, { isSearchEnabled: true })
128
+
129
+ render(<Searcher id={widgetId} labels={{ placeholder: 'Search...' }} />)
130
+
131
+ const input = screen.getByPlaceholderText('Search...')
132
+ expect(input).toBeTruthy()
133
+ })
134
+
135
+ test('updates store with debounced filtered data', async () => {
136
+ // Initialize widget with data and enabled state
137
+ useWidgetStore
138
+ .getState()
139
+ .setWidget(widgetId, { data: mockData, isSearchEnabled: true })
140
+
141
+ render(<Searcher id={widgetId} />)
142
+
143
+ const input = screen.getByRole('textbox')
144
+ fireEvent.change(input, { target: { value: 'Electronics' } })
145
+
146
+ // Wait for debounce (300ms) and flush all pending promises
147
+ await act(async () => {
148
+ await vi.advanceTimersByTimeAsync(300)
149
+ })
150
+
151
+ // Execute the tool pipeline to apply the filter transformation
152
+ await useWidgetStore.getState().executeToolPipeline(widgetId, mockData)
153
+
154
+ const widget = useWidgetStore.getState().getWidget(widgetId)
155
+ const filteredData = widget?.data as EchartWidgetData | undefined
156
+
157
+ // Should only have Electronics item
158
+ expect(filteredData?.[0]?.length).toBe(1)
159
+ expect(filteredData?.[0]?.[0]?.name).toBe('Electronics')
160
+ })
161
+
162
+ test('shows clear button when there is text', () => {
163
+ useWidgetStore.getState().setWidget(widgetId, { isSearchEnabled: true })
164
+
165
+ render(<Searcher id={widgetId} />)
166
+
167
+ const input = screen.getByRole('textbox')
168
+ fireEvent.change(input, { target: { value: 'test' } })
169
+
170
+ const clearButton = screen.getByRole('button', { name: 'Clear search' })
171
+ expect(clearButton).toBeTruthy()
172
+ })
173
+
174
+ test('does not show clear button when input is empty', () => {
175
+ useWidgetStore.getState().setWidget(widgetId, { isSearchEnabled: true })
176
+
177
+ render(<Searcher id={widgetId} />)
178
+
179
+ const clearButton = screen.queryByRole('button', { name: 'Clear search' })
180
+ expect(clearButton).toBeNull()
181
+ })
182
+
183
+ test('clears search text and restores original data when clear button is clicked', () => {
184
+ // Initialize widget with data and enabled state
185
+ useWidgetStore
186
+ .getState()
187
+ .setWidget(widgetId, { data: mockData, isSearchEnabled: true })
188
+
189
+ render(<Searcher id={widgetId} />)
190
+
191
+ const input = screen.getByRole('textbox')
192
+ fireEvent.change(input, { target: { value: 'Electronics' } })
193
+
194
+ // Wait for debounce
195
+ act(() => {
196
+ vi.advanceTimersByTime(300)
197
+ })
198
+
199
+ // Click clear
200
+ const clearButton = screen.getByRole('button', { name: 'Clear search' })
201
+ fireEvent.click(clearButton)
202
+
203
+ // Data should be restored to original
204
+ const widget = useWidgetStore.getState().getWidget(widgetId)
205
+ const restoredData = widget?.data as EchartWidgetData | undefined
206
+ expect(restoredData?.[0]?.length).toBe(3)
207
+ })
208
+
209
+ test('uses custom clear button aria label', () => {
210
+ useWidgetStore.getState().setWidget(widgetId, { isSearchEnabled: true })
211
+
212
+ render(
213
+ <Searcher
214
+ id={widgetId}
215
+ labels={{ clearAriaLabel: 'Limpiar búsqueda' }}
216
+ />,
217
+ )
218
+
219
+ const input = screen.getByRole('textbox')
220
+ fireEvent.change(input, { target: { value: 'text' } })
221
+
222
+ const clearButton = screen.getByRole('button', {
223
+ name: 'Limpiar búsqueda',
224
+ })
225
+ expect(clearButton).toBeTruthy()
226
+ })
227
+
228
+ test('uses custom filter function', async () => {
229
+ // Custom filter that only matches exact values
230
+ const customFilterFn = async (
231
+ data: EchartWidgetData,
232
+ searchText: string,
233
+ ) => {
234
+ return Promise.resolve(
235
+ data.map((series) =>
236
+ series.filter((item) => item.value === parseInt(searchText, 10)),
237
+ ),
238
+ )
239
+ }
240
+
241
+ // Initialize widget with data and enabled state
242
+ useWidgetStore
243
+ .getState()
244
+ .setWidget(widgetId, { data: mockData, isSearchEnabled: true })
245
+
246
+ render(<Searcher id={widgetId} filterFn={customFilterFn} />)
247
+
248
+ const input = screen.getByRole('textbox')
249
+ fireEvent.change(input, { target: { value: '440' } })
250
+
251
+ // Wait for debounce and flush all pending promises
252
+ await act(async () => {
253
+ await vi.advanceTimersByTimeAsync(300)
254
+ })
255
+
256
+ // Execute the tool pipeline to apply the filter transformation
257
+ await useWidgetStore.getState().executeToolPipeline(widgetId, mockData)
258
+
259
+ // Should only have Electronics (value: 440)
260
+ const widget = useWidgetStore.getState().getWidget(widgetId)
261
+ const filteredData = widget?.data as EchartWidgetData | undefined
262
+ expect(filteredData?.[0]?.length).toBe(1)
263
+ expect(filteredData?.[0]?.[0]?.value).toBe(440)
264
+ })
265
+ })
266
+
267
+ describe('SearcherToggle + Searcher integration', () => {
268
+ const widgetId = 'test-integration'
269
+
270
+ beforeEach(() => {
271
+ useWidgetStore.getState().clearWidgets()
272
+ })
273
+
274
+ // Helper component that uses both SearcherToggle and Searcher with same id
275
+ function IntegrationTest() {
276
+ return (
277
+ <>
278
+ <SearcherToggle id={widgetId} />
279
+ <Searcher id={widgetId} />
280
+ </>
281
+ )
282
+ }
283
+
284
+ test('searcher becomes visible when toggle is clicked', () => {
285
+ render(<IntegrationTest />)
286
+
287
+ // Initially searcher should not be visible
288
+ expect(screen.queryByRole('textbox')).toBeNull()
289
+
290
+ // Click toggle
291
+ const toggleButton = screen.getByRole('button')
292
+ fireEvent.click(toggleButton)
293
+
294
+ // Searcher should now be visible
295
+ expect(screen.getByRole('textbox')).toBeTruthy()
296
+ })
297
+
298
+ test('searcher is hidden when toggle is clicked again', () => {
299
+ render(<IntegrationTest />)
300
+
301
+ const toggleButton = screen.getByRole('button')
302
+
303
+ // Enable
304
+ fireEvent.click(toggleButton)
305
+ expect(screen.getByRole('textbox')).toBeTruthy()
306
+
307
+ // Disable
308
+ fireEvent.click(toggleButton)
309
+ expect(screen.queryByRole('textbox')).toBeNull()
310
+ })
311
+
312
+ test('toggle button shows correct active state', () => {
313
+ render(<IntegrationTest />)
314
+
315
+ const toggleButton = screen.getByRole('button')
316
+
317
+ // Initially inactive
318
+ expect(toggleButton.getAttribute('data-active')).toBe('false')
319
+
320
+ // Enable - should be active
321
+ fireEvent.click(toggleButton)
322
+ expect(toggleButton.getAttribute('data-active')).toBe('true')
323
+
324
+ // Disable - should be inactive
325
+ fireEvent.click(toggleButton)
326
+ expect(toggleButton.getAttribute('data-active')).toBe('false')
327
+ })
328
+
329
+ test('store state is correctly updated when toggling', () => {
330
+ render(<IntegrationTest />)
331
+
332
+ const toggleButton = screen.getByRole('button')
333
+
334
+ // Initially should be false
335
+ expect(
336
+ useWidgetStore.getState().getWidget<SearcherState>(widgetId)
337
+ ?.isSearchEnabled,
338
+ ).toBe(false)
339
+
340
+ // Enable
341
+ fireEvent.click(toggleButton)
342
+ expect(
343
+ useWidgetStore.getState().getWidget<SearcherState>(widgetId)
344
+ ?.isSearchEnabled,
345
+ ).toBe(true)
346
+
347
+ // Disable
348
+ fireEvent.click(toggleButton)
349
+ expect(
350
+ useWidgetStore.getState().getWidget<SearcherState>(widgetId)
351
+ ?.isSearchEnabled,
352
+ ).toBe(false)
353
+ })
354
+ })
@@ -0,0 +1,73 @@
1
+ import { IconButton } from '@mui/material'
2
+ import { SearchOutlined } from '@mui/icons-material'
3
+ import { useCallback, useEffect } from 'react'
4
+ import type { SearcherToggleProps, SearcherState } from './types'
5
+ import { actionButtonStyles } from '../shared/styles'
6
+ import { Tooltip } from '../../../components'
7
+ import { useWidgetStore } from '../../stores/widget-store'
8
+ import { useShallow } from 'zustand/shallow'
9
+
10
+ /**
11
+ * Widget action button to toggle search functionality.
12
+ *
13
+ * Stores the enabled state in the widget store using the provided id.
14
+ * When search is active, the button shows an active state with a light background.
15
+ * Use in conjunction with the Searcher component to display the search input.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <SearcherToggle id="my-widget" />
20
+ * <Searcher
21
+ * id="my-widget"
22
+ * data={widgetData}
23
+ * />
24
+ * ```
25
+ */
26
+ export function SearcherToggle({
27
+ id,
28
+ defaultEnabled = false,
29
+ labels,
30
+ Icon,
31
+ IconButtonProps,
32
+ }: SearcherToggleProps) {
33
+ const setWidget = useWidgetStore((state) => state.setWidget)
34
+ const getWidget = useWidgetStore((state) => state.getWidget)
35
+ const storeIsEnabled = useWidgetStore(
36
+ useShallow((state) => state.getWidget<SearcherState>(id)?.isSearchEnabled),
37
+ )
38
+
39
+ const isEnabled = storeIsEnabled ?? defaultEnabled
40
+
41
+ // Initialize store with default value on mount
42
+ useEffect(() => {
43
+ const currentValue = getWidget<SearcherState>(id)?.isSearchEnabled
44
+ if (currentValue === undefined) {
45
+ setWidget(id, { isSearchEnabled: defaultEnabled })
46
+ }
47
+ }, [defaultEnabled, getWidget, id, setWidget])
48
+
49
+ const handleToggle = useCallback(() => {
50
+ setWidget(id, { isSearchEnabled: !isEnabled })
51
+ }, [id, isEnabled, setWidget])
52
+
53
+ const tooltipLabel = isEnabled
54
+ ? (labels?.disable ?? 'Disable search')
55
+ : (labels?.enable ?? 'Enable search')
56
+
57
+ const ariaLabel = labels?.ariaLabel ?? tooltipLabel
58
+
59
+ return (
60
+ <Tooltip title={tooltipLabel}>
61
+ <IconButton
62
+ size='small'
63
+ aria-label={ariaLabel}
64
+ onClick={handleToggle}
65
+ sx={actionButtonStyles.trigger}
66
+ data-active={isEnabled}
67
+ {...IconButtonProps}
68
+ >
69
+ {Icon ?? <SearchOutlined fontSize='small' />}
70
+ </IconButton>
71
+ </Tooltip>
72
+ )
73
+ }
@@ -0,0 +1,205 @@
1
+ import { TextField, InputAdornment, IconButton } from '@mui/material'
2
+ import { ClearOutlined, SearchOutlined } from '@mui/icons-material'
3
+ import { useEffect, useRef, useCallback } from 'react'
4
+ import { useWidgetStore } from '../../stores/widget-store'
5
+ import type { SearcherProps, SearcherFilterFn, SearcherState } from './types'
6
+ import type { EchartWidgetData } from '../../echart/types'
7
+ import { LOCK_SELECTION_TOOL_ID } from '../lock-selection/lock-selection'
8
+
9
+ export const SEARCHER_TOOL_ID = 'searcher'
10
+
11
+ const DEBOUNCE_DELAY = 300
12
+
13
+ /**
14
+ * Search input component that works with SearcherToggle.
15
+ *
16
+ * Registers a transformation tool in the widget pipeline when mounted.
17
+ * Reads the enabled state from the widget store using the provided id.
18
+ * Uses a debounced search to filter data via the transformation pipeline.
19
+ * Auto-focuses when enabled becomes true.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * <SearcherToggle id="my-widget" />
24
+ * <Searcher
25
+ * id="my-widget"
26
+ * order={10}
27
+ * labels={{ placeholder: 'Search categories...' }}
28
+ * />
29
+ * ```
30
+ */
31
+ export function Searcher({
32
+ id,
33
+ filterFn,
34
+ order = 20,
35
+ labels,
36
+ TextFieldProps,
37
+ ClearIcon,
38
+ debounceDelay = DEBOUNCE_DELAY,
39
+ }: SearcherProps) {
40
+ const inputRef = useRef<HTMLInputElement>(null)
41
+ const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
42
+
43
+ // Read enabled state and search text from widget store
44
+ const widgetState = useWidgetStore((state) =>
45
+ state.getWidget<SearcherState>(id),
46
+ )
47
+ const enabled = widgetState?.isSearchEnabled ?? false
48
+ const searchText = widgetState?.searchText ?? ''
49
+ const prevEnabledRef = useRef(enabled)
50
+
51
+ const filter = filterFn ?? defaultFilterFn
52
+
53
+ const setWidget = useWidgetStore((state) => state.setWidget)
54
+ const registerTool = useWidgetStore((state) => state.registerTool)
55
+ const unregisterTool = useWidgetStore((state) => state.unregisterTool)
56
+ const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
57
+ const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
58
+
59
+ const setSearchText = useCallback(
60
+ (text: string) => {
61
+ setWidget(id, { searchText: text })
62
+ },
63
+ [id, setWidget],
64
+ )
65
+
66
+ // Register tool on mount
67
+ useEffect(() => {
68
+ registerTool(id, {
69
+ id: SEARCHER_TOOL_ID,
70
+ order,
71
+ enabled,
72
+ fn: async (data, config) => {
73
+ const searchTextFromConfig = (config?.searchText as string) || ''
74
+
75
+ // Execute filter (can be sync or async)
76
+ const result = filter(data as EchartWidgetData, searchTextFromConfig)
77
+
78
+ // Return result directly (pipeline will handle Promise)
79
+ return result
80
+ },
81
+ config: { searchText },
82
+ disables: [LOCK_SELECTION_TOOL_ID],
83
+ })
84
+
85
+ return () => unregisterTool(id, SEARCHER_TOOL_ID)
86
+ }, [id, order, filter, registerTool, unregisterTool, enabled, searchText])
87
+
88
+ // Update enabled flag when it changes
89
+ useEffect(() => {
90
+ setToolEnabled(id, SEARCHER_TOOL_ID, enabled)
91
+ }, [id, enabled, setToolEnabled])
92
+
93
+ // Update config when search text changes (debounced)
94
+ const debouncedUpdateConfig = useCallback(
95
+ (text: string) => {
96
+ if (debounceTimeoutRef.current) {
97
+ clearTimeout(debounceTimeoutRef.current)
98
+ }
99
+ debounceTimeoutRef.current = setTimeout(() => {
100
+ updateToolConfig(id, SEARCHER_TOOL_ID, { searchText: text })
101
+ }, debounceDelay)
102
+ },
103
+ [id, updateToolConfig, debounceDelay],
104
+ )
105
+
106
+ // Auto-focus when enabled becomes true
107
+ useEffect(() => {
108
+ // Transition from disabled to enabled - focus input
109
+ if (enabled && !prevEnabledRef.current && inputRef.current) {
110
+ inputRef.current.focus()
111
+ }
112
+
113
+ prevEnabledRef.current = enabled
114
+ }, [enabled])
115
+
116
+ // Cleanup debounce timeout on unmount
117
+ useEffect(() => {
118
+ return () => {
119
+ if (debounceTimeoutRef.current) {
120
+ clearTimeout(debounceTimeoutRef.current)
121
+ }
122
+ }
123
+ }, [])
124
+
125
+ const handleChange = useCallback(
126
+ (event: React.ChangeEvent<HTMLInputElement>) => {
127
+ const newValue = event.target.value
128
+ setSearchText(newValue)
129
+ debouncedUpdateConfig(newValue)
130
+ },
131
+ [debouncedUpdateConfig, setSearchText],
132
+ )
133
+
134
+ const handleClear = useCallback(() => {
135
+ setSearchText('')
136
+ updateToolConfig(id, SEARCHER_TOOL_ID, { searchText: '' })
137
+ if (inputRef.current) {
138
+ inputRef.current.focus()
139
+ }
140
+ }, [id, setSearchText, updateToolConfig])
141
+
142
+ if (!enabled) {
143
+ return null
144
+ }
145
+
146
+ const placeholder = labels?.placeholder ?? 'Search...'
147
+ const clearAriaLabel = labels?.clearAriaLabel ?? 'Clear search'
148
+
149
+ return (
150
+ <TextField
151
+ inputRef={inputRef}
152
+ size='small'
153
+ fullWidth
154
+ variant='filled'
155
+ placeholder={placeholder}
156
+ value={searchText}
157
+ onChange={handleChange}
158
+ InputProps={{
159
+ startAdornment: (
160
+ <InputAdornment position='start'>
161
+ <SearchOutlined />
162
+ </InputAdornment>
163
+ ),
164
+ endAdornment: searchText ? (
165
+ <InputAdornment position='end'>
166
+ <IconButton
167
+ size='small'
168
+ aria-label={clearAriaLabel}
169
+ onClick={handleClear}
170
+ edge='end'
171
+ >
172
+ {ClearIcon ?? <ClearOutlined fontSize='small' />}
173
+ </IconButton>
174
+ </InputAdornment>
175
+ ) : null,
176
+ }}
177
+ {...TextFieldProps}
178
+ />
179
+ )
180
+ }
181
+
182
+ /**
183
+ * Default filter function that searches all string fields case-insensitively.
184
+ * Note: Should be synchronous for the new pipeline architecture.
185
+ */
186
+ const defaultFilterFn: SearcherFilterFn = (
187
+ data: EchartWidgetData,
188
+ searchText: string,
189
+ ) => {
190
+ if (!searchText.trim()) return Promise.resolve(data)
191
+
192
+ const lowerSearch = searchText.toLowerCase()
193
+
194
+ return Promise.resolve(
195
+ data.map((series) =>
196
+ series.filter((item) =>
197
+ Object.values(item).some(
198
+ (value) =>
199
+ typeof value === 'string' &&
200
+ value.toLowerCase().includes(lowerSearch),
201
+ ),
202
+ ),
203
+ ),
204
+ )
205
+ }
@@ -0,0 +1,72 @@
1
+ import type { IconButtonProps, TextFieldProps } from '@mui/material'
2
+ import type { ReactNode } from 'react'
3
+ import type { EchartWidgetData } from '../../echart/types'
4
+ import type { BaseWidgetState } from '../../stores/types'
5
+
6
+ /**
7
+ * Filter function type for custom search filtering logic.
8
+ * Receives the original data and search text, returns filtered data.
9
+ * Can be synchronous or asynchronous to support remote filtering.
10
+ */
11
+ export type SearcherFilterFn = (
12
+ data: EchartWidgetData,
13
+ searchText: string,
14
+ ) => Promise<EchartWidgetData> | EchartWidgetData
15
+
16
+ /**
17
+ * Searcher-specific state properties.
18
+ */
19
+ export interface SearcherStateProps {
20
+ /** Whether search is currently enabled */
21
+ isSearchEnabled?: boolean
22
+ /** Current search text */
23
+ searchText?: string
24
+ }
25
+
26
+ /**
27
+ * Widget state extension for searcher functionality.
28
+ * Extends the base widget state with search-specific properties.
29
+ */
30
+ export type SearcherState<T = object> = BaseWidgetState<T & SearcherStateProps>
31
+
32
+ export interface SearcherToggleProps {
33
+ /** Widget ID to store search enabled state */
34
+ id: string
35
+ /** Initial search enabled state. Defaults to false */
36
+ defaultEnabled?: boolean
37
+ /** Custom labels for the action */
38
+ labels?: {
39
+ /** Tooltip when search is disabled (button will enable search) */
40
+ enable?: string
41
+ /** Tooltip when search is enabled (button will disable search) */
42
+ disable?: string
43
+ /** Accessibility label */
44
+ ariaLabel?: string
45
+ }
46
+ /** Props passed to the IconButton component */
47
+ IconButtonProps?: IconButtonProps
48
+ /** Custom icon to display */
49
+ Icon?: ReactNode
50
+ }
51
+
52
+ export interface SearcherProps {
53
+ /** Widget ID to update filtered data in the widget store */
54
+ id: string
55
+ /** Custom filter function. Defaults to case-insensitive search across all string fields */
56
+ filterFn?: SearcherFilterFn
57
+ /** Execution order in the tool pipeline. Lower values execute first. Defaults to 10. */
58
+ order?: number
59
+ /** Custom labels for the searcher input */
60
+ labels?: {
61
+ /** Placeholder text for the input */
62
+ placeholder?: string
63
+ /** Accessibility label for clear button */
64
+ clearAriaLabel?: string
65
+ }
66
+ /** Props passed to the TextField component */
67
+ TextFieldProps?: Omit<TextFieldProps, 'value' | 'onChange' | 'size'>
68
+ /** Custom icon for clear button */
69
+ ClearIcon?: ReactNode
70
+ /** Debounce delay in milliseconds for search input. Defaults to 300ms */
71
+ debounceDelay?: number
72
+ }