@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,504 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { renderHook, act } from '@testing-library/react'
3
+ import { useSelection } from './use-selection'
4
+ import { useWidgetStore } from '../../stores/widget-store'
5
+ import type { TableRow } from '../types'
6
+
7
+ describe('useSelection', () => {
8
+ const testRows: TableRow[] = [
9
+ { id: 1, name: 'Alice' },
10
+ { id: 2, name: 'Bob' },
11
+ { id: 3, name: 'Charlie' },
12
+ { id: 4, name: 'David' },
13
+ ]
14
+
15
+ // Clear widget store before each test
16
+ beforeEach(() => {
17
+ useWidgetStore.getState().clearWidgets()
18
+ })
19
+
20
+ describe('initialization', () => {
21
+ it('should initialize with empty selection', () => {
22
+ const { result } = renderHook(() => useSelection('test-widget'))
23
+
24
+ expect(result.current.selectedIds.length).toBe(0)
25
+ })
26
+
27
+ it('should initialize with provided selection', () => {
28
+ const initialIds = new Set<string | number>([1, 2])
29
+ const { result } = renderHook(() => useSelection('test-widget'))
30
+
31
+ // Set initial selection using selectRows
32
+ act(() => {
33
+ result.current.selectRows(Array.from(initialIds))
34
+ })
35
+
36
+ expect(result.current.selectedIds.length).toBe(2)
37
+ expect(result.current.isSelected(1)).toBe(true)
38
+ expect(result.current.isSelected(2)).toBe(true)
39
+ expect(result.current.isSelected(3)).toBe(false)
40
+ })
41
+ })
42
+
43
+ describe('isSelected', () => {
44
+ it('should return true for selected row', () => {
45
+ const { result } = renderHook(() => useSelection('test-widget'))
46
+
47
+ act(() => {
48
+ result.current.selectRows([1])
49
+ })
50
+
51
+ expect(result.current.isSelected(1)).toBe(true)
52
+ })
53
+
54
+ it('should return false for non-selected row', () => {
55
+ const { result } = renderHook(() => useSelection('test-widget'))
56
+
57
+ act(() => {
58
+ result.current.selectRows([1])
59
+ })
60
+
61
+ expect(result.current.isSelected(2)).toBe(false)
62
+ })
63
+
64
+ it('should handle string IDs', () => {
65
+ const { result } = renderHook(() => useSelection('test-widget'))
66
+
67
+ act(() => {
68
+ result.current.selectRows(['row-1', 'row-2'])
69
+ })
70
+
71
+ expect(result.current.isSelected('row-1')).toBe(true)
72
+ expect(result.current.isSelected('row-3')).toBe(false)
73
+ })
74
+ })
75
+
76
+ describe('isAllSelected', () => {
77
+ it('should return true when all rows are selected', () => {
78
+ const { result } = renderHook(() => useSelection('test-widget'))
79
+
80
+ act(() => {
81
+ result.current.selectRows([1, 2, 3, 4])
82
+ })
83
+
84
+ expect(result.current.isAllSelected(testRows)).toBe(true)
85
+ })
86
+
87
+ it('should return false when some rows are not selected', () => {
88
+ const { result } = renderHook(() => useSelection('test-widget'))
89
+
90
+ act(() => {
91
+ result.current.selectRows([1, 2])
92
+ })
93
+
94
+ expect(result.current.isAllSelected(testRows)).toBe(false)
95
+ })
96
+
97
+ it('should return false when no rows are selected', () => {
98
+ const { result } = renderHook(() => useSelection('test-widget'))
99
+
100
+ expect(result.current.isAllSelected(testRows)).toBe(false)
101
+ })
102
+
103
+ it('should return false for empty rows array', () => {
104
+ const { result } = renderHook(() => useSelection('test-widget'))
105
+
106
+ act(() => {
107
+ result.current.selectRows([1])
108
+ })
109
+
110
+ expect(result.current.isAllSelected([])).toBe(false)
111
+ })
112
+ })
113
+
114
+ describe('isIndeterminate', () => {
115
+ it('should return true when some but not all rows are selected', () => {
116
+ const { result } = renderHook(() => useSelection('test-widget'))
117
+
118
+ act(() => {
119
+ result.current.selectRows([1, 2])
120
+ })
121
+
122
+ expect(result.current.isIndeterminate(testRows)).toBe(true)
123
+ })
124
+
125
+ it('should return false when all rows are selected', () => {
126
+ const { result } = renderHook(() => useSelection('test-widget'))
127
+
128
+ act(() => {
129
+ result.current.selectRows([1, 2, 3, 4])
130
+ })
131
+
132
+ expect(result.current.isIndeterminate(testRows)).toBe(false)
133
+ })
134
+
135
+ it('should return false when no rows are selected', () => {
136
+ const { result } = renderHook(() => useSelection('test-widget'))
137
+
138
+ expect(result.current.isIndeterminate(testRows)).toBe(false)
139
+ })
140
+
141
+ it('should return false for empty rows array', () => {
142
+ const { result } = renderHook(() => useSelection('test-widget'))
143
+
144
+ act(() => {
145
+ result.current.selectRows([1])
146
+ })
147
+
148
+ expect(result.current.isIndeterminate([])).toBe(false)
149
+ })
150
+ })
151
+
152
+ describe('toggleRow', () => {
153
+ it('should select unselected row', () => {
154
+ const { result } = renderHook(() => useSelection('test-widget'))
155
+
156
+ act(() => {
157
+ result.current.toggleRow(1)
158
+ })
159
+
160
+ expect(result.current.isSelected(1)).toBe(true)
161
+ })
162
+
163
+ it('should deselect selected row', () => {
164
+ const { result } = renderHook(() => useSelection('test-widget'))
165
+
166
+ act(() => {
167
+ result.current.selectRows([1])
168
+ })
169
+
170
+ act(() => {
171
+ result.current.toggleRow(1)
172
+ })
173
+
174
+ expect(result.current.isSelected(1)).toBe(false)
175
+ })
176
+
177
+ it('should call onSelectionChange callback', () => {
178
+ const onSelectionChange = vi.fn()
179
+ const { result } = renderHook(() =>
180
+ useSelection('test-widget', { onSelectionChange }),
181
+ )
182
+
183
+ act(() => {
184
+ result.current.toggleRow(1)
185
+ })
186
+
187
+ expect(onSelectionChange).toHaveBeenCalledWith([1])
188
+ })
189
+
190
+ it('should preserve other selections when toggling', () => {
191
+ const { result } = renderHook(() => useSelection('test-widget'))
192
+
193
+ act(() => {
194
+ result.current.selectRows([1, 2])
195
+ })
196
+
197
+ act(() => {
198
+ result.current.toggleRow(3)
199
+ })
200
+
201
+ expect(result.current.isSelected(1)).toBe(true)
202
+ expect(result.current.isSelected(2)).toBe(true)
203
+ expect(result.current.isSelected(3)).toBe(true)
204
+ })
205
+ })
206
+
207
+ describe('toggleAll', () => {
208
+ it('should select all rows when none are selected', () => {
209
+ const { result } = renderHook(() => useSelection('test-widget'))
210
+
211
+ act(() => {
212
+ result.current.toggleAll(testRows)
213
+ })
214
+
215
+ expect(result.current.isAllSelected(testRows)).toBe(true)
216
+ })
217
+
218
+ it('should select all rows when some are selected', () => {
219
+ const { result } = renderHook(() => useSelection('test-widget'))
220
+
221
+ act(() => {
222
+ result.current.selectRows([1])
223
+ })
224
+
225
+ act(() => {
226
+ result.current.toggleAll(testRows)
227
+ })
228
+
229
+ expect(result.current.isAllSelected(testRows)).toBe(true)
230
+ })
231
+
232
+ it('should deselect all rows when all are selected', () => {
233
+ const { result } = renderHook(() => useSelection('test-widget'))
234
+
235
+ act(() => {
236
+ result.current.selectRows([1, 2, 3, 4])
237
+ })
238
+
239
+ act(() => {
240
+ result.current.toggleAll(testRows)
241
+ })
242
+
243
+ testRows.forEach((row) => {
244
+ expect(result.current.isSelected(row.id)).toBe(false)
245
+ })
246
+ })
247
+
248
+ it('should preserve selections not in provided rows', () => {
249
+ const { result } = renderHook(() => useSelection('test-widget'))
250
+
251
+ act(() => {
252
+ result.current.selectRows([5, 6])
253
+ })
254
+
255
+ act(() => {
256
+ result.current.toggleAll(testRows)
257
+ })
258
+
259
+ // Rows 5 and 6 should still be selected
260
+ expect(result.current.isSelected(5)).toBe(true)
261
+ expect(result.current.isSelected(6)).toBe(true)
262
+ // Test rows should also be selected
263
+ expect(result.current.isAllSelected(testRows)).toBe(true)
264
+ })
265
+
266
+ it('should call onSelectionChange callback', () => {
267
+ const onSelectionChange = vi.fn()
268
+ const { result } = renderHook(() =>
269
+ useSelection('test-widget', { onSelectionChange }),
270
+ )
271
+
272
+ act(() => {
273
+ result.current.toggleAll(testRows)
274
+ })
275
+
276
+ expect(onSelectionChange).toHaveBeenCalled()
277
+ })
278
+ })
279
+
280
+ describe('selectRows', () => {
281
+ it('should select specified rows', () => {
282
+ const { result } = renderHook(() => useSelection('test-widget'))
283
+
284
+ act(() => {
285
+ result.current.selectRows([1, 2])
286
+ })
287
+
288
+ expect(result.current.isSelected(1)).toBe(true)
289
+ expect(result.current.isSelected(2)).toBe(true)
290
+ expect(result.current.isSelected(3)).toBe(false)
291
+ })
292
+
293
+ it('should add to existing selection', () => {
294
+ const { result } = renderHook(() => useSelection('test-widget'))
295
+
296
+ act(() => {
297
+ result.current.selectRows([1])
298
+ })
299
+
300
+ act(() => {
301
+ result.current.selectRows([2, 3])
302
+ })
303
+
304
+ expect(result.current.isSelected(1)).toBe(true)
305
+ expect(result.current.isSelected(2)).toBe(true)
306
+ expect(result.current.isSelected(3)).toBe(true)
307
+ })
308
+
309
+ it('should call onSelectionChange callback', () => {
310
+ const onSelectionChange = vi.fn()
311
+ const { result } = renderHook(() =>
312
+ useSelection('test-widget', { onSelectionChange }),
313
+ )
314
+
315
+ act(() => {
316
+ result.current.selectRows([1, 2])
317
+ })
318
+
319
+ expect(onSelectionChange).toHaveBeenCalledWith([1, 2])
320
+ })
321
+ })
322
+
323
+ describe('deselectRows', () => {
324
+ it('should deselect specified rows', () => {
325
+ const { result } = renderHook(() => useSelection('test-widget'))
326
+
327
+ act(() => {
328
+ result.current.selectRows([1, 2, 3])
329
+ })
330
+
331
+ act(() => {
332
+ result.current.deselectRows([1, 2])
333
+ })
334
+
335
+ expect(result.current.isSelected(1)).toBe(false)
336
+ expect(result.current.isSelected(2)).toBe(false)
337
+ expect(result.current.isSelected(3)).toBe(true)
338
+ })
339
+
340
+ it('should handle deselecting non-selected rows', () => {
341
+ const { result } = renderHook(() => useSelection('test-widget'))
342
+
343
+ act(() => {
344
+ result.current.selectRows([1])
345
+ })
346
+
347
+ act(() => {
348
+ result.current.deselectRows([2, 3])
349
+ })
350
+
351
+ expect(result.current.isSelected(1)).toBe(true)
352
+ expect(result.current.selectedIds.length).toBe(1)
353
+ })
354
+
355
+ it('should call onSelectionChange callback', () => {
356
+ const onSelectionChange = vi.fn()
357
+ const { result } = renderHook(() =>
358
+ useSelection('test-widget', { onSelectionChange }),
359
+ )
360
+
361
+ act(() => {
362
+ result.current.selectRows([1, 2])
363
+ })
364
+
365
+ act(() => {
366
+ result.current.deselectRows([1])
367
+ })
368
+
369
+ expect(onSelectionChange).toHaveBeenCalledWith([2])
370
+ })
371
+ })
372
+
373
+ describe('clearSelection', () => {
374
+ it('should clear all selections', () => {
375
+ const { result } = renderHook(() => useSelection('test-widget'))
376
+
377
+ act(() => {
378
+ result.current.selectRows([1, 2, 3])
379
+ })
380
+
381
+ act(() => {
382
+ result.current.clearSelection()
383
+ })
384
+
385
+ expect(result.current.selectedIds.length).toBe(0)
386
+ })
387
+
388
+ it('should handle clearing empty selection', () => {
389
+ const { result } = renderHook(() => useSelection('test-widget'))
390
+
391
+ act(() => {
392
+ result.current.clearSelection()
393
+ })
394
+
395
+ expect(result.current.selectedIds.length).toBe(0)
396
+ })
397
+
398
+ it('should call onSelectionChange callback', () => {
399
+ const onSelectionChange = vi.fn()
400
+ const { result } = renderHook(() =>
401
+ useSelection('test-widget', { onSelectionChange }),
402
+ )
403
+
404
+ act(() => {
405
+ result.current.selectRows([1, 2])
406
+ })
407
+
408
+ act(() => {
409
+ result.current.clearSelection()
410
+ })
411
+
412
+ expect(onSelectionChange).toHaveBeenCalledWith([])
413
+ })
414
+ })
415
+
416
+ describe('edge cases', () => {
417
+ it('should handle mixed string and number IDs', () => {
418
+ const { result } = renderHook(() => useSelection('test-widget'))
419
+
420
+ act(() => {
421
+ result.current.selectRows([1, 'row-2', 3, 'row-4'])
422
+ })
423
+
424
+ expect(result.current.isSelected(1)).toBe(true)
425
+ expect(result.current.isSelected('row-2')).toBe(true)
426
+ expect(result.current.isSelected(3)).toBe(true)
427
+ expect(result.current.isSelected('row-4')).toBe(true)
428
+ })
429
+
430
+ it('should handle multiple toggle operations', () => {
431
+ const { result } = renderHook(() => useSelection('test-widget'))
432
+
433
+ act(() => {
434
+ result.current.toggleRow(1)
435
+ })
436
+ expect(result.current.isSelected(1)).toBe(true)
437
+
438
+ act(() => {
439
+ result.current.toggleRow(1)
440
+ })
441
+ expect(result.current.isSelected(1)).toBe(false)
442
+
443
+ act(() => {
444
+ result.current.toggleRow(1)
445
+ })
446
+ expect(result.current.isSelected(1)).toBe(true)
447
+ })
448
+
449
+ it('should maintain selection across page changes', () => {
450
+ const page1Rows: TableRow[] = [
451
+ { id: 1, name: 'Alice' },
452
+ { id: 2, name: 'Bob' },
453
+ ]
454
+ const page2Rows: TableRow[] = [
455
+ { id: 3, name: 'Charlie' },
456
+ { id: 4, name: 'David' },
457
+ ]
458
+
459
+ const { result } = renderHook(() => useSelection('test-widget'))
460
+
461
+ // Select on page 1
462
+ act(() => {
463
+ result.current.selectRows([1, 2])
464
+ })
465
+
466
+ // Check selections still valid for page 2 rows
467
+ expect(result.current.isAllSelected(page2Rows)).toBe(false)
468
+ expect(result.current.isIndeterminate(page2Rows)).toBe(false)
469
+
470
+ // Select all on page 2
471
+ act(() => {
472
+ result.current.toggleAll(page2Rows)
473
+ })
474
+
475
+ // All 4 rows should be selected
476
+ expect(result.current.selectedIds.length).toBe(4)
477
+ expect(result.current.isAllSelected([...page1Rows, ...page2Rows])).toBe(
478
+ true,
479
+ )
480
+ })
481
+
482
+ it('should persist state in widget store', () => {
483
+ const { result, unmount } = renderHook(() => useSelection('test-widget'))
484
+
485
+ act(() => {
486
+ result.current.selectRows([1, 2, 3])
487
+ })
488
+
489
+ expect(result.current.selectedIds.length).toBe(3)
490
+
491
+ // Unmount and remount - state should persist
492
+ unmount()
493
+
494
+ const { result: newResult } = renderHook(() =>
495
+ useSelection('test-widget'),
496
+ )
497
+
498
+ expect(newResult.current.selectedIds.length).toBe(3)
499
+ expect(newResult.current.isSelected(1)).toBe(true)
500
+ expect(newResult.current.isSelected(2)).toBe(true)
501
+ expect(newResult.current.isSelected(3)).toBe(true)
502
+ })
503
+ })
504
+ })
@@ -0,0 +1,189 @@
1
+ import { useCallback, useMemo } from 'react'
2
+ import type { TableRow, TableWidgetState } from '../types'
3
+ import { useWidgetStore } from '../../stores/widget-store'
4
+ import { useShallow } from 'zustand/shallow'
5
+
6
+ export interface UseSelectionOptions {
7
+ /** Callback when selection changes */
8
+ onSelectionChange?: (selectedIds: (string | number)[]) => void
9
+ }
10
+
11
+ export interface UseSelectionResult {
12
+ /** Array of selected row IDs */
13
+ selectedIds: (string | number)[]
14
+ /** Check if a row is selected */
15
+ isSelected: (id: string | number) => boolean
16
+ /** Check if all provided rows are selected */
17
+ isAllSelected: (rows: TableRow[]) => boolean
18
+ /** Check if some but not all rows are selected (for indeterminate state) */
19
+ isIndeterminate: (rows: TableRow[]) => boolean
20
+ /** Toggle selection of a single row */
21
+ toggleRow: (id: string | number) => void
22
+ /** Toggle selection of all provided rows */
23
+ toggleAll: (rows: TableRow[]) => void
24
+ /** Select specific rows */
25
+ selectRows: (ids: (string | number)[]) => void
26
+ /** Deselect specific rows */
27
+ deselectRows: (ids: (string | number)[]) => void
28
+ /** Clear all selections */
29
+ clearSelection: () => void
30
+ }
31
+
32
+ /**
33
+ * Hook for managing table row selection
34
+ * Selection persists across pagination and is stored in the widget store
35
+ */
36
+ export function useSelection(
37
+ widgetId: string,
38
+ options?: UseSelectionOptions,
39
+ ): UseSelectionResult {
40
+ const { onSelectionChange } = options ?? {}
41
+
42
+ // Get store actions and state
43
+ const setWidget = useWidgetStore((state) => state.setWidget)
44
+ const getWidget = useWidgetStore((state) => state.getWidget)
45
+ const selected = useWidgetStore(
46
+ useShallow((state) => {
47
+ const widget = state.widgets[widgetId] as TableWidgetState | undefined
48
+ return widget?.selected
49
+ }),
50
+ )
51
+
52
+ // Use store values with fallback to empty array
53
+ const selectedIds = useMemo(() => selected ?? [], [selected])
54
+
55
+ // Check if a row is selected
56
+ const isSelected = useCallback(
57
+ (id: string | number) => selectedIds.includes(id),
58
+ [selectedIds],
59
+ )
60
+
61
+ // Check if all provided rows are selected
62
+ const isAllSelected = useCallback(
63
+ (rows: TableRow[]) => {
64
+ if (rows.length === 0) return false
65
+ return rows.every((row) => selectedIds.includes(row.id))
66
+ },
67
+ [selectedIds],
68
+ )
69
+
70
+ // Check if some but not all rows are selected
71
+ const isIndeterminate = useCallback(
72
+ (rows: TableRow[]) => {
73
+ if (rows.length === 0) return false
74
+ const selectedCount = rows.filter((row) =>
75
+ selectedIds.includes(row.id),
76
+ ).length
77
+ return selectedCount > 0 && selectedCount < rows.length
78
+ },
79
+ [selectedIds],
80
+ )
81
+
82
+ // Toggle selection of a single row
83
+ const toggleRow = useCallback(
84
+ (id: string | number) => {
85
+ const widget = getWidget<TableWidgetState>(widgetId)
86
+ const currentSelected = widget?.selected ?? []
87
+
88
+ const newSelected = currentSelected.includes(id)
89
+ ? currentSelected.filter((s) => s !== id)
90
+ : [...currentSelected, id]
91
+
92
+ setWidget<TableWidgetState>(widgetId, {
93
+ selected: newSelected,
94
+ })
95
+
96
+ if (onSelectionChange) {
97
+ onSelectionChange(newSelected)
98
+ }
99
+ },
100
+ [getWidget, setWidget, widgetId, onSelectionChange],
101
+ )
102
+
103
+ // Toggle selection of all provided rows
104
+ const toggleAll = useCallback(
105
+ (rows: TableRow[]) => {
106
+ const widget = getWidget<TableWidgetState>(widgetId)
107
+ const currentSelected = widget?.selected ?? []
108
+
109
+ const rowIds = rows.map((row) => row.id)
110
+ const allSelected = rowIds.every((id) => currentSelected.includes(id))
111
+
112
+ const newSelected = allSelected
113
+ ? currentSelected.filter((id) => !rowIds.includes(id))
114
+ : [...new Set([...currentSelected, ...rowIds])]
115
+
116
+ setWidget<TableWidgetState>(widgetId, {
117
+ selected: newSelected,
118
+ })
119
+
120
+ if (onSelectionChange) {
121
+ onSelectionChange(newSelected)
122
+ }
123
+ },
124
+ [getWidget, setWidget, widgetId, onSelectionChange],
125
+ )
126
+
127
+ // Select specific rows
128
+ const selectRows = useCallback(
129
+ (ids: (string | number)[]) => {
130
+ const widget = getWidget<TableWidgetState>(widgetId)
131
+ const currentSelected = widget?.selected ?? []
132
+
133
+ const newSelected = [...new Set([...currentSelected, ...ids])]
134
+
135
+ setWidget<TableWidgetState>(widgetId, {
136
+ selected: newSelected,
137
+ })
138
+
139
+ if (onSelectionChange) {
140
+ onSelectionChange(newSelected)
141
+ }
142
+ },
143
+ [getWidget, setWidget, widgetId, onSelectionChange],
144
+ )
145
+
146
+ // Deselect specific rows
147
+ const deselectRows = useCallback(
148
+ (ids: (string | number)[]) => {
149
+ const widget = getWidget<TableWidgetState>(widgetId)
150
+ const currentSelected = widget?.selected ?? []
151
+
152
+ const newSelected = currentSelected.filter((id) => !ids.includes(id))
153
+
154
+ setWidget<TableWidgetState>(widgetId, {
155
+ selected: newSelected,
156
+ })
157
+
158
+ if (onSelectionChange) {
159
+ onSelectionChange(newSelected)
160
+ }
161
+ },
162
+ [getWidget, setWidget, widgetId, onSelectionChange],
163
+ )
164
+
165
+ // Clear all selections
166
+ const clearSelection = useCallback(() => {
167
+ const newSelected: (string | number)[] = []
168
+
169
+ setWidget<TableWidgetState>(widgetId, {
170
+ selected: newSelected,
171
+ })
172
+
173
+ if (onSelectionChange) {
174
+ onSelectionChange(newSelected)
175
+ }
176
+ }, [setWidget, widgetId, onSelectionChange])
177
+
178
+ return {
179
+ selectedIds,
180
+ isSelected,
181
+ isAllSelected,
182
+ isIndeterminate,
183
+ toggleRow,
184
+ toggleAll,
185
+ selectRows,
186
+ deselectRows,
187
+ clearSelection,
188
+ }
189
+ }