@furystack/shades-common-components 10.0.35 → 11.0.0

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 (295) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/esm/components/animations.spec.d.ts +2 -0
  3. package/esm/components/animations.spec.d.ts.map +1 -0
  4. package/esm/components/animations.spec.js +201 -0
  5. package/esm/components/animations.spec.js.map +1 -0
  6. package/esm/components/app-bar-link.js +21 -20
  7. package/esm/components/app-bar-link.js.map +1 -1
  8. package/esm/components/app-bar-link.spec.d.ts +2 -0
  9. package/esm/components/app-bar-link.spec.d.ts.map +1 -0
  10. package/esm/components/app-bar-link.spec.js +252 -0
  11. package/esm/components/app-bar-link.spec.js.map +1 -0
  12. package/esm/components/app-bar.js +21 -21
  13. package/esm/components/app-bar.js.map +1 -1
  14. package/esm/components/app-bar.spec.d.ts +2 -0
  15. package/esm/components/app-bar.spec.d.ts.map +1 -0
  16. package/esm/components/app-bar.spec.js +117 -0
  17. package/esm/components/app-bar.spec.js.map +1 -0
  18. package/esm/components/avatar.d.ts.map +1 -1
  19. package/esm/components/avatar.js +15 -19
  20. package/esm/components/avatar.js.map +1 -1
  21. package/esm/components/avatar.spec.d.ts +2 -0
  22. package/esm/components/avatar.spec.d.ts.map +1 -0
  23. package/esm/components/avatar.spec.js +114 -0
  24. package/esm/components/avatar.spec.js.map +1 -0
  25. package/esm/components/button.d.ts.map +1 -1
  26. package/esm/components/button.js +145 -156
  27. package/esm/components/button.js.map +1 -1
  28. package/esm/components/button.spec.d.ts +2 -0
  29. package/esm/components/button.spec.d.ts.map +1 -0
  30. package/esm/components/button.spec.js +155 -0
  31. package/esm/components/button.spec.js.map +1 -0
  32. package/esm/components/command-palette/command-palette-input.d.ts.map +1 -1
  33. package/esm/components/command-palette/command-palette-input.js +18 -16
  34. package/esm/components/command-palette/command-palette-input.js.map +1 -1
  35. package/esm/components/command-palette/command-palette-input.spec.d.ts +2 -0
  36. package/esm/components/command-palette/command-palette-input.spec.d.ts.map +1 -0
  37. package/esm/components/command-palette/command-palette-input.spec.js +233 -0
  38. package/esm/components/command-palette/command-palette-input.spec.js.map +1 -0
  39. package/esm/components/command-palette/command-palette-manager.spec.d.ts +2 -0
  40. package/esm/components/command-palette/command-palette-manager.spec.d.ts.map +1 -0
  41. package/esm/components/command-palette/command-palette-manager.spec.js +362 -0
  42. package/esm/components/command-palette/command-palette-manager.spec.js.map +1 -0
  43. package/esm/components/command-palette/command-palette-suggestion-list.d.ts.map +1 -1
  44. package/esm/components/command-palette/command-palette-suggestion-list.js +42 -46
  45. package/esm/components/command-palette/command-palette-suggestion-list.js.map +1 -1
  46. package/esm/components/command-palette/command-palette-suggestion-list.spec.d.ts +2 -0
  47. package/esm/components/command-palette/command-palette-suggestion-list.spec.d.ts.map +1 -0
  48. package/esm/components/command-palette/command-palette-suggestion-list.spec.js +376 -0
  49. package/esm/components/command-palette/command-palette-suggestion-list.spec.js.map +1 -0
  50. package/esm/components/command-palette/index.d.ts.map +1 -1
  51. package/esm/components/command-palette/index.js +100 -110
  52. package/esm/components/command-palette/index.js.map +1 -1
  53. package/esm/components/command-palette/index.spec.d.ts +2 -0
  54. package/esm/components/command-palette/index.spec.d.ts.map +1 -0
  55. package/esm/components/command-palette/index.spec.js +509 -0
  56. package/esm/components/command-palette/index.spec.js.map +1 -0
  57. package/esm/components/data-grid/body.js +1 -1
  58. package/esm/components/data-grid/body.js.map +1 -1
  59. package/esm/components/data-grid/body.spec.d.ts +2 -0
  60. package/esm/components/data-grid/body.spec.d.ts.map +1 -0
  61. package/esm/components/data-grid/body.spec.js +228 -0
  62. package/esm/components/data-grid/body.spec.js.map +1 -0
  63. package/esm/components/data-grid/data-grid-row.d.ts.map +1 -1
  64. package/esm/components/data-grid/data-grid-row.js +49 -73
  65. package/esm/components/data-grid/data-grid-row.js.map +1 -1
  66. package/esm/components/data-grid/data-grid-row.spec.d.ts +2 -0
  67. package/esm/components/data-grid/data-grid-row.spec.d.ts.map +1 -0
  68. package/esm/components/data-grid/data-grid-row.spec.js +296 -0
  69. package/esm/components/data-grid/data-grid-row.spec.js.map +1 -0
  70. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  71. package/esm/components/data-grid/data-grid.js +35 -28
  72. package/esm/components/data-grid/data-grid.js.map +1 -1
  73. package/esm/components/data-grid/data-grid.spec.d.ts +2 -0
  74. package/esm/components/data-grid/data-grid.spec.d.ts.map +1 -0
  75. package/esm/components/data-grid/data-grid.spec.js +544 -0
  76. package/esm/components/data-grid/data-grid.spec.js.map +1 -0
  77. package/esm/components/data-grid/footer.js +21 -15
  78. package/esm/components/data-grid/footer.js.map +1 -1
  79. package/esm/components/data-grid/footer.spec.d.ts +2 -0
  80. package/esm/components/data-grid/footer.spec.d.ts.map +1 -0
  81. package/esm/components/data-grid/footer.spec.js +264 -0
  82. package/esm/components/data-grid/footer.spec.js.map +1 -0
  83. package/esm/components/data-grid/header.d.ts.map +1 -1
  84. package/esm/components/data-grid/header.js +55 -33
  85. package/esm/components/data-grid/header.js.map +1 -1
  86. package/esm/components/data-grid/header.spec.d.ts +2 -0
  87. package/esm/components/data-grid/header.spec.d.ts.map +1 -0
  88. package/esm/components/data-grid/header.spec.js +421 -0
  89. package/esm/components/data-grid/header.spec.js.map +1 -0
  90. package/esm/components/data-grid/selection-cell.d.ts.map +1 -1
  91. package/esm/components/data-grid/selection-cell.js +13 -6
  92. package/esm/components/data-grid/selection-cell.js.map +1 -1
  93. package/esm/components/data-grid/selection-cell.spec.d.ts +2 -0
  94. package/esm/components/data-grid/selection-cell.spec.d.ts.map +1 -0
  95. package/esm/components/data-grid/selection-cell.spec.js +118 -0
  96. package/esm/components/data-grid/selection-cell.spec.js.map +1 -0
  97. package/esm/components/fab.d.ts.map +1 -1
  98. package/esm/components/fab.js +10 -1
  99. package/esm/components/fab.js.map +1 -1
  100. package/esm/components/fab.spec.d.ts +2 -0
  101. package/esm/components/fab.spec.d.ts.map +1 -0
  102. package/esm/components/fab.spec.js +95 -0
  103. package/esm/components/fab.spec.js.map +1 -0
  104. package/esm/components/form.spec.d.ts +2 -0
  105. package/esm/components/form.spec.d.ts.map +1 -0
  106. package/esm/components/form.spec.js +314 -0
  107. package/esm/components/form.spec.js.map +1 -0
  108. package/esm/components/grid.d.ts.map +1 -1
  109. package/esm/components/grid.js +40 -37
  110. package/esm/components/grid.js.map +1 -1
  111. package/esm/components/grid.spec.d.ts +2 -0
  112. package/esm/components/grid.spec.d.ts.map +1 -0
  113. package/esm/components/grid.spec.js +316 -0
  114. package/esm/components/grid.spec.js.map +1 -0
  115. package/esm/components/inputs/autocomplete.spec.d.ts +2 -0
  116. package/esm/components/inputs/autocomplete.spec.d.ts.map +1 -0
  117. package/esm/components/inputs/autocomplete.spec.js +194 -0
  118. package/esm/components/inputs/autocomplete.spec.js.map +1 -0
  119. package/esm/components/inputs/input.d.ts.map +1 -1
  120. package/esm/components/inputs/input.js +141 -109
  121. package/esm/components/inputs/input.js.map +1 -1
  122. package/esm/components/inputs/input.spec.d.ts +2 -0
  123. package/esm/components/inputs/input.spec.d.ts.map +1 -0
  124. package/esm/components/inputs/input.spec.js +577 -0
  125. package/esm/components/inputs/input.spec.js.map +1 -0
  126. package/esm/components/inputs/text-area.d.ts.map +1 -1
  127. package/esm/components/inputs/text-area.js +54 -58
  128. package/esm/components/inputs/text-area.js.map +1 -1
  129. package/esm/components/inputs/text-area.spec.d.ts +2 -0
  130. package/esm/components/inputs/text-area.spec.d.ts.map +1 -0
  131. package/esm/components/inputs/text-area.spec.js +214 -0
  132. package/esm/components/inputs/text-area.spec.js.map +1 -0
  133. package/esm/components/loader.js +1 -1
  134. package/esm/components/loader.js.map +1 -1
  135. package/esm/components/loader.spec.d.ts +2 -0
  136. package/esm/components/loader.spec.d.ts.map +1 -0
  137. package/esm/components/loader.spec.js +251 -0
  138. package/esm/components/loader.spec.js.map +1 -0
  139. package/esm/components/modal.d.ts.map +1 -1
  140. package/esm/components/modal.js +11 -9
  141. package/esm/components/modal.js.map +1 -1
  142. package/esm/components/modal.spec.d.ts +2 -0
  143. package/esm/components/modal.spec.d.ts.map +1 -0
  144. package/esm/components/modal.spec.js +227 -0
  145. package/esm/components/modal.spec.js.map +1 -0
  146. package/esm/components/noty-list.d.ts.map +1 -1
  147. package/esm/components/noty-list.js +39 -40
  148. package/esm/components/noty-list.js.map +1 -1
  149. package/esm/components/noty-list.spec.d.ts +2 -0
  150. package/esm/components/noty-list.spec.d.ts.map +1 -0
  151. package/esm/components/noty-list.spec.js +486 -0
  152. package/esm/components/noty-list.spec.js.map +1 -0
  153. package/esm/components/paper.d.ts.map +1 -1
  154. package/esm/components/paper.js +15 -12
  155. package/esm/components/paper.js.map +1 -1
  156. package/esm/components/paper.spec.d.ts +2 -0
  157. package/esm/components/paper.spec.d.ts.map +1 -0
  158. package/esm/components/paper.spec.js +63 -0
  159. package/esm/components/paper.spec.js.map +1 -0
  160. package/esm/components/skeleton.js +1 -1
  161. package/esm/components/skeleton.js.map +1 -1
  162. package/esm/components/skeleton.spec.d.ts +2 -0
  163. package/esm/components/skeleton.spec.d.ts.map +1 -0
  164. package/esm/components/skeleton.spec.js +159 -0
  165. package/esm/components/skeleton.spec.js.map +1 -0
  166. package/esm/components/styles.spec.d.ts +2 -0
  167. package/esm/components/styles.spec.d.ts.map +1 -0
  168. package/esm/components/styles.spec.js +56 -0
  169. package/esm/components/styles.spec.js.map +1 -0
  170. package/esm/components/suggest/index.d.ts.map +1 -1
  171. package/esm/components/suggest/index.js +74 -83
  172. package/esm/components/suggest/index.js.map +1 -1
  173. package/esm/components/suggest/index.spec.d.ts +2 -0
  174. package/esm/components/suggest/index.spec.d.ts.map +1 -0
  175. package/esm/components/suggest/index.spec.js +515 -0
  176. package/esm/components/suggest/index.spec.js.map +1 -0
  177. package/esm/components/suggest/suggest-input.d.ts.map +1 -1
  178. package/esm/components/suggest/suggest-input.js +16 -17
  179. package/esm/components/suggest/suggest-input.js.map +1 -1
  180. package/esm/components/suggest/suggest-input.spec.d.ts +2 -0
  181. package/esm/components/suggest/suggest-input.spec.d.ts.map +1 -0
  182. package/esm/components/suggest/suggest-input.spec.js +138 -0
  183. package/esm/components/suggest/suggest-input.spec.js.map +1 -0
  184. package/esm/components/suggest/suggest-manager.spec.d.ts +2 -0
  185. package/esm/components/suggest/suggest-manager.spec.d.ts.map +1 -0
  186. package/esm/components/suggest/suggest-manager.spec.js +308 -0
  187. package/esm/components/suggest/suggest-manager.spec.js.map +1 -0
  188. package/esm/components/suggest/suggestion-list.d.ts.map +1 -1
  189. package/esm/components/suggest/suggestion-list.js +43 -48
  190. package/esm/components/suggest/suggestion-list.js.map +1 -1
  191. package/esm/components/suggest/suggestion-list.spec.d.ts +2 -0
  192. package/esm/components/suggest/suggestion-list.spec.d.ts.map +1 -0
  193. package/esm/components/suggest/suggestion-list.spec.js +252 -0
  194. package/esm/components/suggest/suggestion-list.spec.js.map +1 -0
  195. package/esm/components/tabs.d.ts.map +1 -1
  196. package/esm/components/tabs.js +32 -18
  197. package/esm/components/tabs.js.map +1 -1
  198. package/esm/components/tabs.spec.d.ts +2 -0
  199. package/esm/components/tabs.spec.d.ts.map +1 -0
  200. package/esm/components/tabs.spec.js +187 -0
  201. package/esm/components/tabs.spec.js.map +1 -0
  202. package/esm/components/wizard/index.d.ts.map +1 -1
  203. package/esm/components/wizard/index.js +10 -7
  204. package/esm/components/wizard/index.js.map +1 -1
  205. package/esm/components/wizard/index.spec.d.ts +2 -0
  206. package/esm/components/wizard/index.spec.d.ts.map +1 -0
  207. package/esm/components/wizard/index.spec.js +171 -0
  208. package/esm/components/wizard/index.spec.js.map +1 -0
  209. package/esm/services/collection-service.spec.js +391 -2
  210. package/esm/services/collection-service.spec.js.map +1 -1
  211. package/esm/services/css-variable-theme.d.ts.map +1 -1
  212. package/esm/services/css-variable-theme.js +21 -1
  213. package/esm/services/css-variable-theme.js.map +1 -1
  214. package/esm/services/css-variable-theme.spec.d.ts +2 -0
  215. package/esm/services/css-variable-theme.spec.d.ts.map +1 -0
  216. package/esm/services/css-variable-theme.spec.js +169 -0
  217. package/esm/services/css-variable-theme.spec.js.map +1 -0
  218. package/esm/services/default-palette.d.ts +4 -0
  219. package/esm/services/default-palette.d.ts.map +1 -1
  220. package/esm/services/default-palette.js +22 -0
  221. package/esm/services/default-palette.js.map +1 -1
  222. package/esm/services/theme-provider-service.d.ts +59 -1
  223. package/esm/services/theme-provider-service.d.ts.map +1 -1
  224. package/esm/services/theme-provider-service.js.map +1 -1
  225. package/esm/services/theme-provider-service.spec.d.ts +2 -0
  226. package/esm/services/theme-provider-service.spec.d.ts.map +1 -0
  227. package/esm/services/theme-provider-service.spec.js +166 -0
  228. package/esm/services/theme-provider-service.spec.js.map +1 -0
  229. package/package.json +2 -2
  230. package/src/components/animations.spec.ts +299 -0
  231. package/src/components/app-bar-link.spec.tsx +341 -0
  232. package/src/components/app-bar-link.tsx +21 -21
  233. package/src/components/app-bar.spec.tsx +142 -0
  234. package/src/components/app-bar.tsx +22 -22
  235. package/src/components/avatar.spec.tsx +146 -0
  236. package/src/components/avatar.tsx +17 -20
  237. package/src/components/button.spec.tsx +193 -0
  238. package/src/components/button.tsx +162 -197
  239. package/src/components/command-palette/command-palette-input.spec.tsx +320 -0
  240. package/src/components/command-palette/command-palette-input.tsx +19 -22
  241. package/src/components/command-palette/command-palette-manager.spec.ts +470 -0
  242. package/src/components/command-palette/command-palette-suggestion-list.spec.tsx +499 -0
  243. package/src/components/command-palette/command-palette-suggestion-list.tsx +42 -46
  244. package/src/components/command-palette/index.spec.tsx +684 -0
  245. package/src/components/command-palette/index.tsx +107 -136
  246. package/src/components/data-grid/body.spec.tsx +340 -0
  247. package/src/components/data-grid/body.tsx +1 -1
  248. package/src/components/data-grid/data-grid-row.spec.tsx +382 -0
  249. package/src/components/data-grid/data-grid-row.tsx +50 -82
  250. package/src/components/data-grid/data-grid.spec.tsx +939 -0
  251. package/src/components/data-grid/data-grid.tsx +38 -35
  252. package/src/components/data-grid/footer.spec.tsx +344 -0
  253. package/src/components/data-grid/footer.tsx +19 -19
  254. package/src/components/data-grid/header.spec.tsx +563 -0
  255. package/src/components/data-grid/header.tsx +53 -44
  256. package/src/components/data-grid/selection-cell.spec.tsx +150 -0
  257. package/src/components/data-grid/selection-cell.tsx +12 -6
  258. package/src/components/fab.spec.tsx +108 -0
  259. package/src/components/fab.tsx +10 -1
  260. package/src/components/form.spec.tsx +481 -0
  261. package/src/components/grid.spec.tsx +334 -0
  262. package/src/components/grid.tsx +57 -63
  263. package/src/components/inputs/autocomplete.spec.tsx +258 -0
  264. package/src/components/inputs/input.spec.tsx +808 -0
  265. package/src/components/inputs/input.tsx +153 -139
  266. package/src/components/inputs/text-area.spec.tsx +285 -0
  267. package/src/components/inputs/text-area.tsx +53 -79
  268. package/src/components/loader.spec.tsx +346 -0
  269. package/src/components/loader.tsx +1 -1
  270. package/src/components/modal.spec.tsx +304 -0
  271. package/src/components/modal.tsx +11 -9
  272. package/src/components/noty-list.spec.tsx +631 -0
  273. package/src/components/noty-list.tsx +39 -50
  274. package/src/components/paper.spec.tsx +72 -0
  275. package/src/components/paper.tsx +15 -13
  276. package/src/components/skeleton.spec.tsx +219 -0
  277. package/src/components/skeleton.tsx +1 -1
  278. package/src/components/styles.spec.ts +70 -0
  279. package/src/components/suggest/index.spec.tsx +861 -0
  280. package/src/components/suggest/index.tsx +74 -101
  281. package/src/components/suggest/suggest-input.spec.tsx +181 -0
  282. package/src/components/suggest/suggest-input.tsx +16 -24
  283. package/src/components/suggest/suggest-manager.spec.ts +409 -0
  284. package/src/components/suggest/suggestion-list.spec.tsx +334 -0
  285. package/src/components/suggest/suggestion-list.tsx +43 -48
  286. package/src/components/tabs.spec.tsx +236 -0
  287. package/src/components/tabs.tsx +33 -21
  288. package/src/components/wizard/index.spec.tsx +224 -0
  289. package/src/components/wizard/index.tsx +10 -9
  290. package/src/services/collection-service.spec.ts +492 -3
  291. package/src/services/css-variable-theme.spec.ts +204 -0
  292. package/src/services/css-variable-theme.ts +21 -1
  293. package/src/services/default-palette.ts +22 -0
  294. package/src/services/theme-provider-service.spec.ts +195 -0
  295. package/src/services/theme-provider-service.ts +60 -2
@@ -0,0 +1,334 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, initializeShadeRoot } from '@furystack/shades'
3
+ import { using } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import { SuggestManager } from './suggest-manager.js'
6
+ import { SuggestionList } from './suggestion-list.js'
7
+ import type { SuggestionResult } from './suggestion-result.js'
8
+
9
+ type TestEntry = { id: number; name: string }
10
+
11
+ const createTestEntries = (): TestEntry[] => [
12
+ { id: 1, name: 'alpha' },
13
+ { id: 2, name: 'beta' },
14
+ { id: 3, name: 'gamma' },
15
+ ]
16
+
17
+ const createSuggestionResult = (entry: TestEntry): SuggestionResult => ({
18
+ element: (<span>{entry.name}</span>) as unknown as JSX.Element,
19
+ score: entry.id,
20
+ })
21
+
22
+ describe('SuggestionList', () => {
23
+ let originalAnimate: typeof Element.prototype.animate
24
+
25
+ beforeEach(() => {
26
+ document.body.innerHTML = '<div id="root"></div>'
27
+ vi.useFakeTimers()
28
+ originalAnimate = Element.prototype.animate
29
+
30
+ Element.prototype.animate = vi.fn(() => {
31
+ const mockAnimation = {
32
+ onfinish: null as ((event: AnimationPlaybackEvent) => void) | null,
33
+ oncancel: null as ((event: AnimationPlaybackEvent) => void) | null,
34
+ cancel: vi.fn(),
35
+ play: vi.fn(),
36
+ pause: vi.fn(),
37
+ finish: vi.fn(),
38
+ addEventListener: vi.fn(),
39
+ removeEventListener: vi.fn(),
40
+ }
41
+ return mockAnimation as unknown as Animation
42
+ }) as typeof Element.prototype.animate
43
+ })
44
+
45
+ afterEach(() => {
46
+ document.body.innerHTML = ''
47
+ Element.prototype.animate = originalAnimate
48
+ vi.useRealTimers()
49
+ vi.restoreAllMocks()
50
+ })
51
+
52
+ const createManager = () => {
53
+ const getEntries = vi.fn().mockResolvedValue(createTestEntries())
54
+ const getSuggestionEntry = vi.fn().mockImplementation(createSuggestionResult)
55
+ return new SuggestManager<TestEntry>(getEntries, getSuggestionEntry)
56
+ }
57
+
58
+ it('should render with shadow DOM', async () => {
59
+ const injector = new Injector()
60
+ const rootElement = document.getElementById('root') as HTMLDivElement
61
+
62
+ using(createManager(), (manager) => {
63
+ initializeShadeRoot({
64
+ injector,
65
+ rootElement,
66
+ jsxElement: <SuggestionList manager={manager} />,
67
+ })
68
+ })
69
+
70
+ await vi.advanceTimersByTimeAsync(50)
71
+
72
+ const suggestionList = document.querySelector('shade-suggest-suggestion-list')
73
+ expect(suggestionList).not.toBeNull()
74
+ })
75
+
76
+ it('should render the suggestions container', async () => {
77
+ const injector = new Injector()
78
+ const rootElement = document.getElementById('root') as HTMLDivElement
79
+
80
+ using(createManager(), (manager) => {
81
+ initializeShadeRoot({
82
+ injector,
83
+ rootElement,
84
+ jsxElement: <SuggestionList manager={manager} />,
85
+ })
86
+ })
87
+
88
+ await vi.advanceTimersByTimeAsync(50)
89
+
90
+ const container = document.querySelector('.suggestion-items-container')
91
+ expect(container).not.toBeNull()
92
+ })
93
+
94
+ it('should render suggestion items when suggestions are present', async () => {
95
+ const injector = new Injector()
96
+ const rootElement = document.getElementById('root') as HTMLDivElement
97
+ const manager = createManager()
98
+
99
+ initializeShadeRoot({
100
+ injector,
101
+ rootElement,
102
+ jsxElement: <SuggestionList manager={manager} />,
103
+ })
104
+
105
+ await vi.advanceTimersByTimeAsync(50)
106
+
107
+ void manager.getSuggestion({ injector, term: 'test' })
108
+ await vi.advanceTimersByTimeAsync(250)
109
+ await vi.advanceTimersByTimeAsync(50)
110
+
111
+ const suggestionItems = document.querySelectorAll('.suggestion-item')
112
+ expect(suggestionItems.length).toBe(3)
113
+
114
+ expect(suggestionItems[0].textContent).toContain('alpha')
115
+ expect(suggestionItems[1].textContent).toContain('beta')
116
+ expect(suggestionItems[2].textContent).toContain('gamma')
117
+
118
+ manager[Symbol.dispose]()
119
+ })
120
+
121
+ it('should apply selected class to the correct suggestion item', async () => {
122
+ const injector = new Injector()
123
+ const rootElement = document.getElementById('root') as HTMLDivElement
124
+ const manager = createManager()
125
+
126
+ initializeShadeRoot({
127
+ injector,
128
+ rootElement,
129
+ jsxElement: <SuggestionList manager={manager} />,
130
+ })
131
+
132
+ await vi.advanceTimersByTimeAsync(50)
133
+
134
+ void manager.getSuggestion({ injector, term: 'test' })
135
+ await vi.advanceTimersByTimeAsync(250)
136
+ await vi.advanceTimersByTimeAsync(50)
137
+
138
+ const suggestionItems = document.querySelectorAll('.suggestion-item')
139
+ expect(suggestionItems[0].classList.contains('selected')).toBe(true)
140
+ expect(suggestionItems[1].classList.contains('selected')).toBe(false)
141
+ expect(suggestionItems[2].classList.contains('selected')).toBe(false)
142
+
143
+ manager[Symbol.dispose]()
144
+ })
145
+
146
+ it('should update selected class when selectedIndex changes', async () => {
147
+ const injector = new Injector()
148
+ const rootElement = document.getElementById('root') as HTMLDivElement
149
+ const manager = createManager()
150
+
151
+ initializeShadeRoot({
152
+ injector,
153
+ rootElement,
154
+ jsxElement: <SuggestionList manager={manager} />,
155
+ })
156
+
157
+ await vi.advanceTimersByTimeAsync(50)
158
+
159
+ void manager.getSuggestion({ injector, term: 'test' })
160
+ await vi.advanceTimersByTimeAsync(250)
161
+ await vi.advanceTimersByTimeAsync(50)
162
+
163
+ manager.selectedIndex.setValue(1)
164
+ await vi.advanceTimersByTimeAsync(50)
165
+
166
+ const suggestionItems = document.querySelectorAll('.suggestion-item')
167
+ expect(suggestionItems[0].classList.contains('selected')).toBe(false)
168
+ expect(suggestionItems[1].classList.contains('selected')).toBe(true)
169
+ expect(suggestionItems[2].classList.contains('selected')).toBe(false)
170
+
171
+ manager.selectedIndex.setValue(2)
172
+ await vi.advanceTimersByTimeAsync(50)
173
+
174
+ expect(suggestionItems[0].classList.contains('selected')).toBe(false)
175
+ expect(suggestionItems[1].classList.contains('selected')).toBe(false)
176
+ expect(suggestionItems[2].classList.contains('selected')).toBe(true)
177
+
178
+ manager[Symbol.dispose]()
179
+ })
180
+
181
+ it('should call selectSuggestion when a suggestion item is clicked', async () => {
182
+ const injector = new Injector()
183
+ const rootElement = document.getElementById('root') as HTMLDivElement
184
+ const manager = createManager()
185
+ const selectSpy = vi.spyOn(manager, 'selectSuggestion')
186
+
187
+ initializeShadeRoot({
188
+ injector,
189
+ rootElement,
190
+ jsxElement: <SuggestionList manager={manager} />,
191
+ })
192
+
193
+ await vi.advanceTimersByTimeAsync(50)
194
+
195
+ void manager.getSuggestion({ injector, term: 'test' })
196
+ await vi.advanceTimersByTimeAsync(250)
197
+ await vi.advanceTimersByTimeAsync(50)
198
+
199
+ manager.isOpened.setValue(true)
200
+ await vi.advanceTimersByTimeAsync(50)
201
+
202
+ const suggestionItems = document.querySelectorAll('.suggestion-item')
203
+ ;(suggestionItems[1] as HTMLElement).click()
204
+
205
+ expect(selectSpy).toHaveBeenCalledWith(1)
206
+
207
+ manager[Symbol.dispose]()
208
+ })
209
+
210
+ it('should not call selectSuggestion when list is not opened', async () => {
211
+ const injector = new Injector()
212
+ const rootElement = document.getElementById('root') as HTMLDivElement
213
+ const manager = createManager()
214
+
215
+ initializeShadeRoot({
216
+ injector,
217
+ rootElement,
218
+ jsxElement: <SuggestionList manager={manager} />,
219
+ })
220
+
221
+ await vi.advanceTimersByTimeAsync(50)
222
+
223
+ manager.currentSuggestions.setValue([
224
+ { entry: { id: 1, name: 'alpha' }, suggestion: createSuggestionResult({ id: 1, name: 'alpha' }) },
225
+ { entry: { id: 2, name: 'beta' }, suggestion: createSuggestionResult({ id: 2, name: 'beta' }) },
226
+ ])
227
+ await vi.advanceTimersByTimeAsync(50)
228
+
229
+ const selectSpy = vi.spyOn(manager, 'selectSuggestion')
230
+
231
+ const suggestionItems = document.querySelectorAll('.suggestion-item')
232
+ ;(suggestionItems[1] as HTMLElement).click()
233
+
234
+ expect(selectSpy).not.toHaveBeenCalled()
235
+
236
+ manager[Symbol.dispose]()
237
+ })
238
+
239
+ it('should render empty container when no suggestions', async () => {
240
+ const injector = new Injector()
241
+ const rootElement = document.getElementById('root') as HTMLDivElement
242
+
243
+ const getEntries = vi.fn().mockResolvedValue([])
244
+ const getSuggestionEntry = vi.fn().mockImplementation(createSuggestionResult)
245
+ const manager = new SuggestManager<TestEntry>(getEntries, getSuggestionEntry)
246
+
247
+ initializeShadeRoot({
248
+ injector,
249
+ rootElement,
250
+ jsxElement: <SuggestionList manager={manager} />,
251
+ })
252
+
253
+ await vi.advanceTimersByTimeAsync(50)
254
+
255
+ const suggestionItems = document.querySelectorAll('.suggestion-item')
256
+ expect(suggestionItems.length).toBe(0)
257
+
258
+ manager[Symbol.dispose]()
259
+ })
260
+
261
+ describe('animations', () => {
262
+ it('should animate container when isOpened changes to true', async () => {
263
+ const injector = new Injector()
264
+ const rootElement = document.getElementById('root') as HTMLDivElement
265
+ const manager = createManager()
266
+
267
+ initializeShadeRoot({
268
+ injector,
269
+ rootElement,
270
+ jsxElement: <SuggestionList manager={manager} />,
271
+ })
272
+
273
+ await vi.advanceTimersByTimeAsync(50)
274
+
275
+ const container = document.querySelector('.suggestion-items-container') as HTMLDivElement
276
+
277
+ manager.isOpened.setValue(true)
278
+ await vi.advanceTimersByTimeAsync(50)
279
+
280
+ expect(container.style.zIndex).toBe('1')
281
+
282
+ manager[Symbol.dispose]()
283
+ })
284
+
285
+ it('should animate container when isOpened changes to false', async () => {
286
+ const injector = new Injector()
287
+ const rootElement = document.getElementById('root') as HTMLDivElement
288
+ const manager = createManager()
289
+
290
+ initializeShadeRoot({
291
+ injector,
292
+ rootElement,
293
+ jsxElement: <SuggestionList manager={manager} />,
294
+ })
295
+
296
+ await vi.advanceTimersByTimeAsync(50)
297
+
298
+ manager.isOpened.setValue(true)
299
+ await vi.advanceTimersByTimeAsync(50)
300
+
301
+ const container = document.querySelector('.suggestion-items-container') as HTMLDivElement
302
+
303
+ manager.isOpened.setValue(false)
304
+ await vi.advanceTimersByTimeAsync(50)
305
+
306
+ expect(container.style.zIndex).toBe('-1')
307
+
308
+ manager[Symbol.dispose]()
309
+ })
310
+ })
311
+
312
+ describe('container width', () => {
313
+ it('should set container width based on parent element', async () => {
314
+ const injector = new Injector()
315
+ const rootElement = document.getElementById('root') as HTMLDivElement
316
+ rootElement.style.width = '400px'
317
+
318
+ const manager = createManager()
319
+
320
+ initializeShadeRoot({
321
+ injector,
322
+ rootElement,
323
+ jsxElement: <SuggestionList manager={manager} />,
324
+ })
325
+
326
+ await vi.advanceTimersByTimeAsync(50)
327
+
328
+ const container = document.querySelector('.suggestion-items-container') as HTMLDivElement
329
+ expect(container.style.width).toBeDefined()
330
+
331
+ manager[Symbol.dispose]()
332
+ })
333
+ })
334
+ })
@@ -1,31 +1,59 @@
1
1
  import type { ChildrenList } from '@furystack/shades'
2
2
  import { Shade, createComponent } from '@furystack/shades'
3
- import { ThemeProviderService } from '../../services/theme-provider-service.js'
3
+ import { cssVariableTheme } from '../../services/css-variable-theme.js'
4
4
  import { promisifyAnimation } from '../../utils/promisify-animation.js'
5
5
  import type { SuggestManager } from './suggest-manager.js'
6
6
 
7
7
  export const SuggestionList: <T>(props: { manager: SuggestManager<T> }, children: ChildrenList) => JSX.Element<any> =
8
8
  Shade<{ manager: SuggestManager<any> }>({
9
9
  shadowDomName: 'shade-suggest-suggestion-list',
10
- render: ({ element, props, injector, useObservable }) => {
10
+ css: {
11
+ '& .suggestion-items-container': {
12
+ borderTop: 'none',
13
+ position: 'absolute',
14
+ borderRadius: '0px 0px 12px 12px',
15
+ marginLeft: '14px',
16
+ marginTop: '4px',
17
+ overflow: 'hidden',
18
+ zIndex: '1',
19
+ left: 'auto',
20
+ backgroundColor: cssVariableTheme.background.paper,
21
+ color: cssVariableTheme.text.secondary,
22
+ boxShadow: '0 8px 24px rgba(0,0,0,0.18), 0 2px 8px rgba(0,0,0,0.12)',
23
+ backdropFilter: 'blur(20px)',
24
+ border: '1px solid rgba(128,128,128,0.2)',
25
+ },
26
+ '& .suggestion-item': {
27
+ padding: '0.875em 1.25em',
28
+ cursor: 'pointer',
29
+ background: 'transparent',
30
+ fontWeight: '400',
31
+ borderLeft: '3px solid transparent',
32
+ transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
33
+ fontSize: '0.95em',
34
+ letterSpacing: '0.01em',
35
+ },
36
+ '& .suggestion-item:hover': {
37
+ background: 'rgba(128,128,128,0.1)',
38
+ },
39
+ '& .suggestion-item.selected': {
40
+ background: 'rgba(128,128,128,0.2)',
41
+ fontWeight: '500',
42
+ borderLeft: `3px solid ${cssVariableTheme.text.primary}`,
43
+ },
44
+ '& .suggestion-item.selected:hover': {
45
+ background: 'rgba(128,128,128,0.2)',
46
+ },
47
+ },
48
+ render: ({ element, props, useObservable }) => {
11
49
  const { manager } = props
12
- const { theme } = injector.getInstance(ThemeProviderService)
13
50
 
14
51
  const [suggestions] = useObservable('suggestions', manager.currentSuggestions)
15
52
 
16
- // todo: GetLast is eliminated, do we need it?
17
53
  const [selectedIndex] = useObservable('selectedIndex', manager.selectedIndex, {
18
54
  onChange: (idx) => {
19
- ;([...element.querySelectorAll('.suggestion-item')] as HTMLDivElement[]).map((s, i) => {
20
- if (i === idx) {
21
- s.style.background = 'rgba(128,128,128,0.2)'
22
- s.style.fontWeight = '500'
23
- s.style.borderLeft = `3px solid ${theme.text.primary}`
24
- } else {
25
- s.style.background = 'transparent'
26
- s.style.fontWeight = '400'
27
- s.style.borderLeft = '3px solid transparent'
28
- }
55
+ ;([...element.querySelectorAll('.suggestion-item')] as HTMLDivElement[]).forEach((s, i) => {
56
+ s.classList.toggle('selected', i === idx)
29
57
  })
30
58
  },
31
59
  })
@@ -64,50 +92,17 @@ export const SuggestionList: <T>(props: { manager: SuggestManager<T> }, children
64
92
  <div
65
93
  className="suggestion-items-container"
66
94
  style={{
67
- borderTop: 'none',
68
- position: 'absolute',
69
- borderRadius: '0px 0px 12px 12px',
70
- marginLeft: '14px',
71
- marginTop: '4px',
72
- overflow: 'hidden',
73
- zIndex: '1',
74
- left: 'auto',
75
- backgroundColor: theme.background.paper,
76
- color: theme.text.secondary,
77
- boxShadow: '0 8px 24px rgba(0,0,0,0.18), 0 2px 8px rgba(0,0,0,0.12)',
78
- backdropFilter: 'blur(20px)',
79
- border: '1px solid rgba(128,128,128,0.2)',
80
95
  width: `calc(${Math.round(element.parentElement?.getBoundingClientRect().width || 200)}px - 3em)`,
81
96
  }}
82
97
  >
83
98
  {suggestions.map((s, i) => (
84
99
  <div
85
- className="suggestion-item"
100
+ className={`suggestion-item${i === selectedIndex ? ' selected' : ''}`}
86
101
  onclick={() => {
87
102
  if (isListOpened) {
88
103
  manager.selectSuggestion(i)
89
104
  }
90
105
  }}
91
- onmouseenter={(ev) => {
92
- if (i !== selectedIndex) {
93
- ;(ev.target as HTMLElement).style.background = 'rgba(128,128,128,0.1)'
94
- }
95
- }}
96
- onmouseleave={(ev) => {
97
- if (i !== selectedIndex) {
98
- ;(ev.target as HTMLElement).style.background = 'transparent'
99
- }
100
- }}
101
- style={{
102
- padding: '0.875em 1.25em',
103
- cursor: 'pointer',
104
- background: i === selectedIndex ? 'rgba(128,128,128,0.2)' : 'transparent',
105
- fontWeight: i === selectedIndex ? '500' : '400',
106
- borderLeft: i === selectedIndex ? `3px solid ${theme.text.primary}` : '3px solid transparent',
107
- transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
108
- fontSize: '0.95em',
109
- letterSpacing: '0.01em',
110
- }}
111
106
  >
112
107
  {s.suggestion.element}
113
108
  </div>
@@ -0,0 +1,236 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, initializeShadeRoot, LocationService } from '@furystack/shades'
3
+ import { sleepAsync, usingAsync } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
+ import { Tabs, type Tab } from './tabs.js'
6
+
7
+ describe('Tabs', () => {
8
+ beforeEach(() => {
9
+ document.body.innerHTML = '<div id="root"></div>'
10
+ window.location.hash = ''
11
+ })
12
+
13
+ afterEach(() => {
14
+ document.body.innerHTML = ''
15
+ window.location.hash = ''
16
+ })
17
+
18
+ const createTabs = (): Tab[] => [
19
+ {
20
+ hash: 'tab1',
21
+ header: <span>Tab 1</span>,
22
+ component: <div id="content-1">Content 1</div>,
23
+ },
24
+ {
25
+ hash: 'tab2',
26
+ header: <span>Tab 2</span>,
27
+ component: <div id="content-2">Content 2</div>,
28
+ },
29
+ {
30
+ hash: 'tab3',
31
+ header: <span>Tab 3</span>,
32
+ component: <div id="content-3">Content 3</div>,
33
+ },
34
+ ]
35
+
36
+ it('should render all tab headers', async () => {
37
+ await usingAsync(new Injector(), async (injector) => {
38
+ const rootElement = document.getElementById('root') as HTMLDivElement
39
+ const tabs = createTabs()
40
+
41
+ initializeShadeRoot({
42
+ injector,
43
+ rootElement,
44
+ jsxElement: <Tabs tabs={tabs} />,
45
+ })
46
+
47
+ await sleepAsync(100)
48
+
49
+ expect(document.body.innerHTML).toContain('Tab 1')
50
+ expect(document.body.innerHTML).toContain('Tab 2')
51
+ expect(document.body.innerHTML).toContain('Tab 3')
52
+ })
53
+ })
54
+
55
+ it('should display the active tab content based on URL hash', async () => {
56
+ await usingAsync(new Injector(), async (injector) => {
57
+ window.location.hash = '#tab2'
58
+
59
+ const rootElement = document.getElementById('root') as HTMLDivElement
60
+ const tabs = createTabs()
61
+
62
+ initializeShadeRoot({
63
+ injector,
64
+ rootElement,
65
+ jsxElement: <Tabs tabs={tabs} />,
66
+ })
67
+
68
+ await sleepAsync(100)
69
+
70
+ expect(document.getElementById('content-2')).toBeTruthy()
71
+ expect(document.getElementById('content-1')).toBeFalsy()
72
+ expect(document.getElementById('content-3')).toBeFalsy()
73
+ })
74
+ })
75
+
76
+ it('should not display any tab content when hash does not match', async () => {
77
+ await usingAsync(new Injector(), async (injector) => {
78
+ window.location.hash = '#nonexistent'
79
+
80
+ const rootElement = document.getElementById('root') as HTMLDivElement
81
+ const tabs = createTabs()
82
+
83
+ initializeShadeRoot({
84
+ injector,
85
+ rootElement,
86
+ jsxElement: <Tabs tabs={tabs} />,
87
+ })
88
+
89
+ await sleepAsync(100)
90
+
91
+ expect(document.getElementById('content-1')).toBeFalsy()
92
+ expect(document.getElementById('content-2')).toBeFalsy()
93
+ expect(document.getElementById('content-3')).toBeFalsy()
94
+ })
95
+ })
96
+
97
+ it('should update active tab when hash changes', async () => {
98
+ await usingAsync(new Injector(), async (injector) => {
99
+ window.location.hash = '#tab1'
100
+
101
+ const rootElement = document.getElementById('root') as HTMLDivElement
102
+ const tabs = createTabs()
103
+
104
+ initializeShadeRoot({
105
+ injector,
106
+ rootElement,
107
+ jsxElement: <Tabs tabs={tabs} />,
108
+ })
109
+
110
+ await sleepAsync(100)
111
+
112
+ expect(document.getElementById('content-1')).toBeTruthy()
113
+
114
+ // Change hash
115
+ window.location.hash = '#tab3'
116
+ injector.getInstance(LocationService).updateState()
117
+
118
+ await sleepAsync(100)
119
+
120
+ expect(document.getElementById('content-3')).toBeTruthy()
121
+ expect(document.getElementById('content-1')).toBeFalsy()
122
+ })
123
+ })
124
+
125
+ it('should render tab headers as anchor elements with correct href', async () => {
126
+ await usingAsync(new Injector(), async (injector) => {
127
+ const rootElement = document.getElementById('root') as HTMLDivElement
128
+ const tabs = createTabs()
129
+
130
+ initializeShadeRoot({
131
+ injector,
132
+ rootElement,
133
+ jsxElement: <Tabs tabs={tabs} />,
134
+ })
135
+
136
+ await sleepAsync(100)
137
+
138
+ // Tab headers extend anchor elements
139
+ const html = document.body.innerHTML
140
+ expect(html).toContain('href="#tab1"')
141
+ expect(html).toContain('href="#tab2"')
142
+ expect(html).toContain('href="#tab3"')
143
+ })
144
+ })
145
+
146
+ it('should mark the active tab header with active class', async () => {
147
+ await usingAsync(new Injector(), async (injector) => {
148
+ window.location.hash = '#tab2'
149
+
150
+ const rootElement = document.getElementById('root') as HTMLDivElement
151
+ const tabs = createTabs()
152
+
153
+ initializeShadeRoot({
154
+ injector,
155
+ rootElement,
156
+ jsxElement: <Tabs tabs={tabs} />,
157
+ })
158
+
159
+ await sleepAsync(100)
160
+
161
+ // The tab header with tab2 hash should have active class
162
+ const html = document.body.innerHTML
163
+ // Verify the active tab content is shown
164
+ expect(document.getElementById('content-2')).toBeTruthy()
165
+ // Check for active class in the tab-header element containing Tab 2
166
+ expect(html).toMatch(/shade-tab-header[^>]*class="active"[^>]*href="#tab2"/)
167
+ })
168
+ })
169
+
170
+ it('should switch active class when hash changes', async () => {
171
+ await usingAsync(new Injector(), async (injector) => {
172
+ window.location.hash = '#tab1'
173
+
174
+ const rootElement = document.getElementById('root') as HTMLDivElement
175
+ const tabs = createTabs()
176
+
177
+ initializeShadeRoot({
178
+ injector,
179
+ rootElement,
180
+ jsxElement: <Tabs tabs={tabs} />,
181
+ })
182
+
183
+ await sleepAsync(100)
184
+
185
+ // Verify tab1 is active
186
+ expect(document.getElementById('content-1')).toBeTruthy()
187
+ expect(document.body.innerHTML).toMatch(/shade-tab-header[^>]*class="active"[^>]*href="#tab1"/)
188
+
189
+ // Change hash
190
+ window.location.hash = '#tab2'
191
+ injector.getInstance(LocationService).updateState()
192
+
193
+ await sleepAsync(100)
194
+
195
+ // Verify tab2 is now active
196
+ expect(document.getElementById('content-2')).toBeTruthy()
197
+ expect(document.getElementById('content-1')).toBeFalsy()
198
+ expect(document.body.innerHTML).toMatch(/shade-tab-header[^>]*class="active"[^>]*href="#tab2"/)
199
+ })
200
+ })
201
+
202
+ it('should apply containerStyle to the element', async () => {
203
+ await usingAsync(new Injector(), async (injector) => {
204
+ const rootElement = document.getElementById('root') as HTMLDivElement
205
+ const tabs = createTabs()
206
+
207
+ initializeShadeRoot({
208
+ injector,
209
+ rootElement,
210
+ jsxElement: <Tabs tabs={tabs} containerStyle={{ maxWidth: '800px' }} />,
211
+ })
212
+
213
+ await sleepAsync(100)
214
+
215
+ const tabsElement = document.querySelector('shade-tabs') as HTMLElement
216
+ expect(tabsElement.style.maxWidth).toBe('800px')
217
+ })
218
+ })
219
+
220
+ it('should work with empty tabs array', async () => {
221
+ await usingAsync(new Injector(), async (injector) => {
222
+ const rootElement = document.getElementById('root') as HTMLDivElement
223
+
224
+ initializeShadeRoot({
225
+ injector,
226
+ rootElement,
227
+ jsxElement: <Tabs tabs={[]} />,
228
+ })
229
+
230
+ await sleepAsync(100)
231
+
232
+ const tabHeaders = document.querySelectorAll('shade-tab-header')
233
+ expect(tabHeaders.length).toBe(0)
234
+ })
235
+ })
236
+ })