@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,391 @@
1
+ import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'
2
+ import { render, screen, waitFor } from '@testing-library/react'
3
+ import { userEvent } from '@testing-library/user-event'
4
+ import { WidgetNote } from './note'
5
+
6
+ describe('WidgetNote', () => {
7
+ let mockResizeObserver: {
8
+ observe: ReturnType<typeof vi.fn>
9
+ disconnect: ReturnType<typeof vi.fn>
10
+ unobserve: ReturnType<typeof vi.fn>
11
+ }
12
+
13
+ beforeEach(() => {
14
+ // Mock ResizeObserver
15
+ mockResizeObserver = {
16
+ observe: vi.fn(),
17
+ disconnect: vi.fn(),
18
+ unobserve: vi.fn(),
19
+ }
20
+
21
+ global.ResizeObserver = class {
22
+ observe = mockResizeObserver.observe
23
+ disconnect = mockResizeObserver.disconnect
24
+ unobserve = mockResizeObserver.unobserve
25
+ } as unknown as typeof ResizeObserver
26
+
27
+ // Default: no overflow
28
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(60)
29
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
30
+ })
31
+
32
+ afterEach(() => {
33
+ vi.restoreAllMocks()
34
+ })
35
+
36
+ test('returns null when children is undefined', () => {
37
+ // @ts-expect-error - testing runtime behavior with undefined
38
+ const { container } = render(<WidgetNote>{undefined}</WidgetNote>)
39
+ expect(container.firstChild).toBeNull()
40
+ })
41
+
42
+ test('returns null when children is empty string', () => {
43
+ const { container } = render(<WidgetNote>{''}</WidgetNote>)
44
+ expect(container.firstChild).toBeNull()
45
+ })
46
+
47
+ test('returns null when children is null', () => {
48
+ // @ts-expect-error - testing runtime behavior with null
49
+ const { container } = render(<WidgetNote>{null}</WidgetNote>)
50
+ expect(container.firstChild).toBeNull()
51
+ })
52
+
53
+ test('renders Markdown component when children exists', () => {
54
+ render(<WidgetNote>Simple note text</WidgetNote>)
55
+ expect(screen.getByText('Simple note text')).toBeTruthy()
56
+ })
57
+
58
+ test('renders markdown-formatted note content', () => {
59
+ render(<WidgetNote>{'# Heading\n\nParagraph text'}</WidgetNote>)
60
+ expect(screen.getByText('Heading')).toBeTruthy()
61
+ expect(screen.getByText('Paragraph text')).toBeTruthy()
62
+ })
63
+
64
+ test('renders note with list items', () => {
65
+ render(<WidgetNote>{'- Item 1\n- Item 2\n- Item 3'}</WidgetNote>)
66
+ expect(screen.getByText('Item 1')).toBeTruthy()
67
+ expect(screen.getByText('Item 2')).toBeTruthy()
68
+ expect(screen.getByText('Item 3')).toBeTruthy()
69
+ })
70
+
71
+ test('renders note with links', () => {
72
+ render(
73
+ <WidgetNote>{'Check out [this link](https://example.com)'}</WidgetNote>,
74
+ )
75
+ const link = screen.getByText('this link')
76
+ expect(link.tagName).toBe('A')
77
+ expect(link.getAttribute('href')).toBe('https://example.com')
78
+ })
79
+
80
+ test('renders note with complex markdown', () => {
81
+ render(
82
+ <WidgetNote>{`# Title
83
+
84
+ ## Subtitle
85
+
86
+ Paragraph with **bold** and *italic*.
87
+
88
+ - List item 1
89
+ - List item 2
90
+
91
+ [Link](https://example.com)`}</WidgetNote>,
92
+ )
93
+ expect(screen.getByText('Title')).toBeTruthy()
94
+ expect(screen.getByText('Subtitle')).toBeTruthy()
95
+ expect(screen.getByText('bold')).toBeTruthy()
96
+ expect(screen.getByText('italic')).toBeTruthy()
97
+ expect(screen.getByText('List item 1')).toBeTruthy()
98
+ expect(screen.getByText('List item 2')).toBeTruthy()
99
+ expect(screen.getByText('Link')).toBeTruthy()
100
+ })
101
+
102
+ test('renders note with bold text', () => {
103
+ render(<WidgetNote>{'This is **bold** text'}</WidgetNote>)
104
+ expect(screen.getByText('bold')).toBeTruthy()
105
+ })
106
+
107
+ test('renders note with italic text', () => {
108
+ render(<WidgetNote>{'This is *italic* text'}</WidgetNote>)
109
+ expect(screen.getByText('italic')).toBeTruthy()
110
+ })
111
+
112
+ test('renders note with code blocks', () => {
113
+ render(<WidgetNote>{'Here is `inline code`'}</WidgetNote>)
114
+ expect(screen.getByText('inline code')).toBeTruthy()
115
+ })
116
+
117
+ test('renders note with blockquote', () => {
118
+ render(<WidgetNote>{'> This is a quote'}</WidgetNote>)
119
+ expect(screen.getByText('This is a quote')).toBeTruthy()
120
+ })
121
+
122
+ test('uses default labels', () => {
123
+ // Mock overflow to show toggle button
124
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(100)
125
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
126
+
127
+ render(<WidgetNote>{'Long content\n'.repeat(10)}</WidgetNote>)
128
+
129
+ expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy()
130
+ })
131
+
132
+ test('uses custom labels', async () => {
133
+ const user = userEvent.setup()
134
+
135
+ // Mock overflow to show toggle button
136
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(100)
137
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
138
+
139
+ render(
140
+ <WidgetNote labels={{ showMore: 'Expand', showLess: 'Collapse' }}>
141
+ {'Long content\n'.repeat(10)}
142
+ </WidgetNote>,
143
+ )
144
+
145
+ const button = screen.getByRole('button', { name: 'Expand' })
146
+ expect(button).toBeTruthy()
147
+
148
+ await user.click(button)
149
+
150
+ await waitFor(() => {
151
+ expect(screen.getByRole('button', { name: 'Collapse' })).toBeTruthy()
152
+ })
153
+ })
154
+
155
+ test('merges custom labels with defaults', async () => {
156
+ const user = userEvent.setup()
157
+
158
+ // Mock overflow to show toggle button
159
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(100)
160
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
161
+
162
+ render(
163
+ <WidgetNote labels={{ showMore: 'Expand' }}>
164
+ {'Long content\n'.repeat(10)}
165
+ </WidgetNote>,
166
+ )
167
+
168
+ const button = await screen.findByRole('button', { name: 'Expand' })
169
+ expect(button).toBeTruthy()
170
+
171
+ await user.click(button)
172
+
173
+ await waitFor(() => {
174
+ // showLess should use default since not provided
175
+ expect(screen.getByRole('button', { name: 'Show Less' })).toBeTruthy()
176
+ })
177
+ })
178
+
179
+ describe('Expand/Collapse functionality', () => {
180
+ test('does not show toggle button when content does not overflow', () => {
181
+ // Content fits within container (no overflow)
182
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(60)
183
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
184
+
185
+ render(<WidgetNote>Short content</WidgetNote>)
186
+
187
+ expect(screen.queryByRole('button')).toBeNull()
188
+ })
189
+
190
+ test('shows toggle button when content overflows', async () => {
191
+ // Content overflows container
192
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
193
+ 100,
194
+ )
195
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
196
+
197
+ render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)
198
+
199
+ await waitFor(() => {
200
+ expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy()
201
+ })
202
+ })
203
+
204
+ test('toggles expansion state on button click', async () => {
205
+ const user = userEvent.setup()
206
+
207
+ // Content overflows container
208
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
209
+ 100,
210
+ )
211
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
212
+
213
+ render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)
214
+
215
+ const button = await screen.findByRole('button', { name: 'Show More' })
216
+ expect(button.getAttribute('aria-expanded')).toBe('false')
217
+
218
+ await user.click(button)
219
+
220
+ await waitFor(() => {
221
+ expect(screen.getByRole('button', { name: 'Show Less' })).toBeTruthy()
222
+ expect(
223
+ screen
224
+ .getByRole('button', { name: 'Show Less' })
225
+ .getAttribute('aria-expanded'),
226
+ ).toBe('true')
227
+ })
228
+ })
229
+
230
+ test('collapses content on second button click', async () => {
231
+ const user = userEvent.setup()
232
+
233
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
234
+ 100,
235
+ )
236
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
237
+
238
+ render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)
239
+
240
+ const button = await screen.findByRole('button', { name: 'Show More' })
241
+
242
+ // Expand
243
+ await user.click(button)
244
+
245
+ await waitFor(() => {
246
+ expect(screen.getByRole('button', { name: 'Show Less' })).toBeTruthy()
247
+ })
248
+
249
+ // Collapse
250
+ await user.click(screen.getByRole('button', { name: 'Show Less' }))
251
+
252
+ await waitFor(() => {
253
+ expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy()
254
+ expect(
255
+ screen
256
+ .getByRole('button', { name: 'Show More' })
257
+ .getAttribute('aria-expanded'),
258
+ ).toBe('false')
259
+ })
260
+ })
261
+
262
+ test('sets up ResizeObserver on mount', () => {
263
+ render(<WidgetNote>Test content</WidgetNote>)
264
+
265
+ // Verify ResizeObserver was used by checking observe was called
266
+ expect(mockResizeObserver.observe).toHaveBeenCalled()
267
+ })
268
+
269
+ test('disconnects ResizeObserver on unmount', () => {
270
+ const { unmount } = render(<WidgetNote>Test content</WidgetNote>)
271
+
272
+ unmount()
273
+
274
+ expect(mockResizeObserver.disconnect).toHaveBeenCalled()
275
+ })
276
+
277
+ test('recalculates overflow when ResizeObserver triggers', async () => {
278
+ let resizeCallback: ResizeObserverCallback
279
+
280
+ global.ResizeObserver = class {
281
+ constructor(callback: ResizeObserverCallback) {
282
+ resizeCallback = callback
283
+ }
284
+ observe = mockResizeObserver.observe
285
+ disconnect = mockResizeObserver.disconnect
286
+ unobserve = mockResizeObserver.unobserve
287
+ } as unknown as typeof ResizeObserver
288
+
289
+ // Start with no overflow
290
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(60)
291
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
292
+
293
+ render(<WidgetNote>Test content</WidgetNote>)
294
+
295
+ expect(screen.queryByRole('button')).toBeNull()
296
+
297
+ // Simulate overflow after resize
298
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
299
+ 100,
300
+ )
301
+
302
+ // Trigger resize callback
303
+ resizeCallback!([], mockResizeObserver as unknown as ResizeObserver)
304
+
305
+ await waitFor(() => {
306
+ expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy()
307
+ })
308
+ })
309
+
310
+ test('maintains expanded state correctly after multiple toggles', async () => {
311
+ const user = userEvent.setup()
312
+
313
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
314
+ 100,
315
+ )
316
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
317
+
318
+ render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)
319
+
320
+ const button = await screen.findByRole('button')
321
+
322
+ // Toggle multiple times
323
+ await user.click(button) // Expand
324
+ await user.click(button) // Collapse
325
+ await user.click(button) // Expand
326
+ await user.click(button) // Collapse
327
+
328
+ await waitFor(() => {
329
+ expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy()
330
+ expect(button.getAttribute('aria-expanded')).toBe('false')
331
+ })
332
+ })
333
+ })
334
+
335
+ describe('Accessibility', () => {
336
+ test('button has aria-expanded attribute', async () => {
337
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
338
+ 100,
339
+ )
340
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
341
+
342
+ render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)
343
+
344
+ const button = await screen.findByRole('button')
345
+ expect(button.getAttribute('aria-expanded')).toBe('false')
346
+ })
347
+
348
+ test('button has aria-controls attribute', async () => {
349
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
350
+ 100,
351
+ )
352
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
353
+
354
+ render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)
355
+
356
+ const button = await screen.findByRole('button')
357
+ expect(button.getAttribute('aria-controls')).toBe('note-content')
358
+ })
359
+
360
+ test('button has aria-label for Show More', async () => {
361
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
362
+ 100,
363
+ )
364
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
365
+
366
+ render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)
367
+
368
+ const button = await screen.findByRole('button', { name: 'Show More' })
369
+ expect(button.getAttribute('aria-label')).toBe('Show More')
370
+ })
371
+
372
+ test('button has aria-label for Show Less when expanded', async () => {
373
+ const user = userEvent.setup()
374
+
375
+ vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
376
+ 100,
377
+ )
378
+ vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
379
+
380
+ render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)
381
+
382
+ const button = await screen.findByRole('button')
383
+ await user.click(button)
384
+
385
+ await waitFor(() => {
386
+ const expandedButton = screen.getByRole('button', { name: 'Show Less' })
387
+ expect(expandedButton.getAttribute('aria-label')).toBe('Show Less')
388
+ })
389
+ })
390
+ })
391
+ })
@@ -0,0 +1,114 @@
1
+ import { useState, useEffect, useRef, useEffectEvent } from 'react'
2
+ import { Box, Button, Link, Typography } from '@mui/material'
3
+ import type { WidgetNoteProps } from './types'
4
+ import { styles } from './style'
5
+ import ReactMarkdown, { type Components } from 'react-markdown'
6
+
7
+ const DEFAULT_LABELS = {
8
+ showMore: 'Show More',
9
+ showLess: 'Show Less',
10
+ } as const
11
+
12
+ const DEFAULT_P = ({ children }: { children?: React.ReactNode }) => (
13
+ <Typography paragraph color='inherit' variant='caption'>
14
+ {children}
15
+ </Typography>
16
+ )
17
+
18
+ const COMPONENTS: Components = {
19
+ h1: DEFAULT_P,
20
+ h2: DEFAULT_P,
21
+ h3: DEFAULT_P,
22
+ p: DEFAULT_P,
23
+ a: ({ children, href, target = '_blank', rel = 'noopener noreferrer' }) => (
24
+ <Link
25
+ href={href}
26
+ target={target}
27
+ rel={rel}
28
+ color='text.primary'
29
+ underline='always'
30
+ >
31
+ {children}
32
+ </Link>
33
+ ),
34
+ img: () => null,
35
+ ul: DEFAULT_P,
36
+ ol: DEFAULT_P,
37
+ li: DEFAULT_P,
38
+ }
39
+
40
+ export function WidgetNote({
41
+ children,
42
+ labels = DEFAULT_LABELS,
43
+ }: WidgetNoteProps) {
44
+ const [isExpanded, setIsExpanded] = useState(false)
45
+ const [shouldShowToggle, setShouldShowToggle] = useState(false)
46
+ const contentRef = useRef<HTMLDivElement>(null)
47
+
48
+ const checkOverflow = useEffectEvent(() => {
49
+ if (contentRef.current) {
50
+ // Check if content overflows 3 lines
51
+ const isOverflowing =
52
+ contentRef.current.scrollHeight > contentRef.current.clientHeight
53
+ setShouldShowToggle(isOverflowing)
54
+ }
55
+ })
56
+
57
+ useEffect(() => {
58
+ checkOverflow()
59
+ }, [children])
60
+
61
+ useEffect(() => {
62
+ const element = contentRef.current
63
+ if (!element) return
64
+
65
+ const resizeObserver = new ResizeObserver(() => {
66
+ checkOverflow()
67
+ })
68
+
69
+ resizeObserver.observe(element)
70
+
71
+ return () => {
72
+ resizeObserver.disconnect()
73
+ }
74
+ }, [])
75
+
76
+ if (!children) {
77
+ return null
78
+ }
79
+
80
+ const handleToggle = () => {
81
+ setIsExpanded(!isExpanded)
82
+ }
83
+
84
+ const mergedLabels = { ...DEFAULT_LABELS, ...labels }
85
+
86
+ return (
87
+ <Box>
88
+ <Box
89
+ ref={contentRef}
90
+ sx={{
91
+ ...styles.root,
92
+ ...(isExpanded ? styles.expanded : styles.clamped),
93
+ }}
94
+ >
95
+ <ReactMarkdown components={COMPONENTS}>{children}</ReactMarkdown>
96
+ </Box>
97
+ {(shouldShowToggle || isExpanded) && (
98
+ <Button
99
+ onClick={handleToggle}
100
+ variant='text'
101
+ color='primary'
102
+ sx={styles.linkButton}
103
+ aria-expanded={isExpanded}
104
+ aria-label={
105
+ isExpanded ? mergedLabels.showLess : mergedLabels.showMore
106
+ }
107
+ aria-controls='note-content'
108
+ >
109
+ {isExpanded ? mergedLabels.showLess : mergedLabels.showMore}
110
+ </Button>
111
+ )}
112
+ </Box>
113
+ )
114
+ }
@@ -0,0 +1,29 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ export const styles = {
4
+ root: {
5
+ typography: 'caption',
6
+ color: (theme: Theme) => theme.palette.text.secondary,
7
+ marginBlockStart: ({ spacing }) => spacing(1),
8
+ },
9
+ clamped: {
10
+ display: '-webkit-box',
11
+ WebkitLineClamp: 3,
12
+ WebkitBoxOrient: 'vertical',
13
+ overflow: 'hidden',
14
+ },
15
+ expanded: {
16
+ display: 'block',
17
+ },
18
+ linkButton: {
19
+ padding: 0,
20
+ minWidth: 'auto',
21
+ textTransform: 'none',
22
+ typography: 'caption',
23
+ fontWeight: 500,
24
+ textDecoration: 'none',
25
+ '&:hover': {
26
+ backgroundColor: 'transparent',
27
+ },
28
+ },
29
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,9 @@
1
+ import type { MarkdownComponentProps } from '../markdown'
2
+
3
+ export interface WidgetNoteProps {
4
+ children: MarkdownComponentProps['children']
5
+ labels?: {
6
+ showMore?: string
7
+ showLess?: string
8
+ }
9
+ }