@furystack/shades-common-components 14.0.0 → 15.0.1

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 (287) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/esm/components/accordion/accordion-item.d.ts.map +1 -1
  3. package/esm/components/accordion/accordion-item.js +6 -9
  4. package/esm/components/accordion/accordion-item.js.map +1 -1
  5. package/esm/components/accordion/accordion.d.ts +7 -0
  6. package/esm/components/accordion/accordion.d.ts.map +1 -1
  7. package/esm/components/accordion/accordion.js +4 -1
  8. package/esm/components/accordion/accordion.js.map +1 -1
  9. package/esm/components/accordion/accordion.spec.js +91 -50
  10. package/esm/components/accordion/accordion.spec.js.map +1 -1
  11. package/esm/components/carousel.js +1 -1
  12. package/esm/components/carousel.js.map +1 -1
  13. package/esm/components/chip.d.ts.map +1 -1
  14. package/esm/components/chip.js +4 -2
  15. package/esm/components/chip.js.map +1 -1
  16. package/esm/components/chip.spec.js +42 -0
  17. package/esm/components/chip.spec.js.map +1 -1
  18. package/esm/components/command-palette/index.d.ts.map +1 -1
  19. package/esm/components/command-palette/index.js +14 -1
  20. package/esm/components/command-palette/index.js.map +1 -1
  21. package/esm/components/command-palette/index.spec.js +78 -33
  22. package/esm/components/command-palette/index.spec.js.map +1 -1
  23. package/esm/components/data-grid/data-grid-row.d.ts.map +1 -1
  24. package/esm/components/data-grid/data-grid-row.js +18 -2
  25. package/esm/components/data-grid/data-grid-row.js.map +1 -1
  26. package/esm/components/data-grid/data-grid.d.ts +7 -0
  27. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  28. package/esm/components/data-grid/data-grid.js +28 -10
  29. package/esm/components/data-grid/data-grid.js.map +1 -1
  30. package/esm/components/data-grid/data-grid.spec.js +114 -34
  31. package/esm/components/data-grid/data-grid.spec.js.map +1 -1
  32. package/esm/components/data-grid/selection-cell.d.ts.map +1 -1
  33. package/esm/components/data-grid/selection-cell.js +1 -1
  34. package/esm/components/data-grid/selection-cell.js.map +1 -1
  35. package/esm/components/dialog.d.ts +11 -0
  36. package/esm/components/dialog.d.ts.map +1 -1
  37. package/esm/components/dialog.js +2 -2
  38. package/esm/components/dialog.js.map +1 -1
  39. package/esm/components/dialog.spec.js +54 -2
  40. package/esm/components/dialog.spec.js.map +1 -1
  41. package/esm/components/dropdown.d.ts.map +1 -1
  42. package/esm/components/dropdown.js +1 -1
  43. package/esm/components/dropdown.js.map +1 -1
  44. package/esm/components/dropdown.spec.js +8 -0
  45. package/esm/components/dropdown.spec.js.map +1 -1
  46. package/esm/components/image.d.ts.map +1 -1
  47. package/esm/components/image.js +15 -6
  48. package/esm/components/image.js.map +1 -1
  49. package/esm/components/image.spec.js +60 -0
  50. package/esm/components/image.spec.js.map +1 -1
  51. package/esm/components/inputs/checkbox.d.ts.map +1 -1
  52. package/esm/components/inputs/checkbox.js +1 -0
  53. package/esm/components/inputs/checkbox.js.map +1 -1
  54. package/esm/components/inputs/radio.d.ts.map +1 -1
  55. package/esm/components/inputs/radio.js +1 -0
  56. package/esm/components/inputs/radio.js.map +1 -1
  57. package/esm/components/inputs/slider.d.ts.map +1 -1
  58. package/esm/components/inputs/slider.js +1 -0
  59. package/esm/components/inputs/slider.js.map +1 -1
  60. package/esm/components/inputs/switch.d.ts.map +1 -1
  61. package/esm/components/inputs/switch.js +1 -0
  62. package/esm/components/inputs/switch.js.map +1 -1
  63. package/esm/components/list/list-item.d.ts.map +1 -1
  64. package/esm/components/list/list-item.js +21 -5
  65. package/esm/components/list/list-item.js.map +1 -1
  66. package/esm/components/list/list.d.ts +7 -0
  67. package/esm/components/list/list.d.ts.map +1 -1
  68. package/esm/components/list/list.js +28 -8
  69. package/esm/components/list/list.js.map +1 -1
  70. package/esm/components/list/list.spec.js +117 -23
  71. package/esm/components/list/list.spec.js.map +1 -1
  72. package/esm/components/markdown/markdown-display.d.ts.map +1 -1
  73. package/esm/components/markdown/markdown-display.js +11 -1
  74. package/esm/components/markdown/markdown-display.js.map +1 -1
  75. package/esm/components/markdown/markdown-display.spec.js +97 -0
  76. package/esm/components/markdown/markdown-display.spec.js.map +1 -1
  77. package/esm/components/markdown/markdown-editor.spec.js +87 -0
  78. package/esm/components/markdown/markdown-editor.spec.js.map +1 -1
  79. package/esm/components/menu/menu.js +1 -1
  80. package/esm/components/menu/menu.js.map +1 -1
  81. package/esm/components/modal.d.ts +10 -0
  82. package/esm/components/modal.d.ts.map +1 -1
  83. package/esm/components/modal.js +24 -4
  84. package/esm/components/modal.js.map +1 -1
  85. package/esm/components/modal.spec.js +86 -1
  86. package/esm/components/modal.spec.js.map +1 -1
  87. package/esm/components/page-layout/index.js +1 -1
  88. package/esm/components/page-layout/index.js.map +1 -1
  89. package/esm/components/page-layout/index.spec.js +14 -0
  90. package/esm/components/page-layout/index.spec.js.map +1 -1
  91. package/esm/components/rating.d.ts.map +1 -1
  92. package/esm/components/rating.js +28 -21
  93. package/esm/components/rating.js.map +1 -1
  94. package/esm/components/rating.spec.js +151 -4
  95. package/esm/components/rating.spec.js.map +1 -1
  96. package/esm/components/suggest/index.d.ts.map +1 -1
  97. package/esm/components/suggest/index.js +14 -1
  98. package/esm/components/suggest/index.js.map +1 -1
  99. package/esm/components/suggest/index.spec.js +98 -43
  100. package/esm/components/suggest/index.spec.js.map +1 -1
  101. package/esm/components/suggest/suggest-manager.js +2 -2
  102. package/esm/components/suggest/suggest-manager.js.map +1 -1
  103. package/esm/components/tabs.d.ts.map +1 -1
  104. package/esm/components/tabs.js +4 -0
  105. package/esm/components/tabs.js.map +1 -1
  106. package/esm/components/tree/tree-item.d.ts.map +1 -1
  107. package/esm/components/tree/tree-item.js +18 -5
  108. package/esm/components/tree/tree-item.js.map +1 -1
  109. package/esm/components/tree/tree.d.ts +7 -0
  110. package/esm/components/tree/tree.d.ts.map +1 -1
  111. package/esm/components/tree/tree.js +12 -3
  112. package/esm/components/tree/tree.js.map +1 -1
  113. package/esm/components/tree/tree.spec.js +64 -2
  114. package/esm/components/tree/tree.spec.js.map +1 -1
  115. package/esm/services/collection-service.d.ts +9 -0
  116. package/esm/services/collection-service.d.ts.map +1 -1
  117. package/esm/services/collection-service.js +33 -11
  118. package/esm/services/collection-service.js.map +1 -1
  119. package/esm/services/collection-service.spec.js +33 -24
  120. package/esm/services/collection-service.spec.js.map +1 -1
  121. package/esm/services/css-variable-theme.d.ts +7 -0
  122. package/esm/services/css-variable-theme.d.ts.map +1 -1
  123. package/esm/services/css-variable-theme.js +23 -0
  124. package/esm/services/css-variable-theme.js.map +1 -1
  125. package/esm/services/css-variable-theme.spec.js +1 -0
  126. package/esm/services/css-variable-theme.spec.js.map +1 -1
  127. package/esm/services/list-service.d.ts +9 -0
  128. package/esm/services/list-service.d.ts.map +1 -1
  129. package/esm/services/list-service.js +13 -13
  130. package/esm/services/list-service.js.map +1 -1
  131. package/esm/services/list-service.spec.js +13 -33
  132. package/esm/services/list-service.spec.js.map +1 -1
  133. package/esm/services/theme-provider-service.d.ts +3 -0
  134. package/esm/services/theme-provider-service.d.ts.map +1 -1
  135. package/esm/services/theme-provider-service.js.map +1 -1
  136. package/esm/services/tree-service.d.ts.map +1 -1
  137. package/esm/services/tree-service.js +5 -9
  138. package/esm/services/tree-service.js.map +1 -1
  139. package/esm/services/tree-service.spec.js +12 -9
  140. package/esm/services/tree-service.spec.js.map +1 -1
  141. package/esm/themes/architect-theme.d.ts +1 -0
  142. package/esm/themes/architect-theme.d.ts.map +1 -1
  143. package/esm/themes/architect-theme.js +1 -0
  144. package/esm/themes/architect-theme.js.map +1 -1
  145. package/esm/themes/auditore-theme.d.ts +1 -0
  146. package/esm/themes/auditore-theme.d.ts.map +1 -1
  147. package/esm/themes/auditore-theme.js +1 -0
  148. package/esm/themes/auditore-theme.js.map +1 -1
  149. package/esm/themes/black-mesa-theme.d.ts +1 -0
  150. package/esm/themes/black-mesa-theme.d.ts.map +1 -1
  151. package/esm/themes/black-mesa-theme.js +1 -0
  152. package/esm/themes/black-mesa-theme.js.map +1 -1
  153. package/esm/themes/chieftain-theme.d.ts +1 -0
  154. package/esm/themes/chieftain-theme.d.ts.map +1 -1
  155. package/esm/themes/chieftain-theme.js +1 -0
  156. package/esm/themes/chieftain-theme.js.map +1 -1
  157. package/esm/themes/default-dark-theme.d.ts +1 -0
  158. package/esm/themes/default-dark-theme.d.ts.map +1 -1
  159. package/esm/themes/default-dark-theme.js +1 -0
  160. package/esm/themes/default-dark-theme.js.map +1 -1
  161. package/esm/themes/default-light-theme.d.ts +1 -0
  162. package/esm/themes/default-light-theme.d.ts.map +1 -1
  163. package/esm/themes/default-light-theme.js +1 -0
  164. package/esm/themes/default-light-theme.js.map +1 -1
  165. package/esm/themes/dragonborn-theme.d.ts +1 -0
  166. package/esm/themes/dragonborn-theme.d.ts.map +1 -1
  167. package/esm/themes/dragonborn-theme.js +1 -0
  168. package/esm/themes/dragonborn-theme.js.map +1 -1
  169. package/esm/themes/hawkins-theme.d.ts +1 -0
  170. package/esm/themes/hawkins-theme.d.ts.map +1 -1
  171. package/esm/themes/hawkins-theme.js +1 -0
  172. package/esm/themes/hawkins-theme.js.map +1 -1
  173. package/esm/themes/jedi-theme.d.ts +1 -0
  174. package/esm/themes/jedi-theme.d.ts.map +1 -1
  175. package/esm/themes/jedi-theme.js +1 -0
  176. package/esm/themes/jedi-theme.js.map +1 -1
  177. package/esm/themes/neon-runner-theme.d.ts +1 -0
  178. package/esm/themes/neon-runner-theme.d.ts.map +1 -1
  179. package/esm/themes/neon-runner-theme.js +1 -0
  180. package/esm/themes/neon-runner-theme.js.map +1 -1
  181. package/esm/themes/paladin-theme.d.ts +1 -0
  182. package/esm/themes/paladin-theme.d.ts.map +1 -1
  183. package/esm/themes/paladin-theme.js +1 -0
  184. package/esm/themes/paladin-theme.js.map +1 -1
  185. package/esm/themes/plumber-theme.d.ts +1 -0
  186. package/esm/themes/plumber-theme.d.ts.map +1 -1
  187. package/esm/themes/plumber-theme.js +1 -0
  188. package/esm/themes/plumber-theme.js.map +1 -1
  189. package/esm/themes/replicant-theme.d.ts +1 -0
  190. package/esm/themes/replicant-theme.d.ts.map +1 -1
  191. package/esm/themes/replicant-theme.js +1 -0
  192. package/esm/themes/replicant-theme.js.map +1 -1
  193. package/esm/themes/sandworm-theme.d.ts +1 -0
  194. package/esm/themes/sandworm-theme.d.ts.map +1 -1
  195. package/esm/themes/sandworm-theme.js +1 -0
  196. package/esm/themes/sandworm-theme.js.map +1 -1
  197. package/esm/themes/shadow-broker-theme.d.ts +1 -0
  198. package/esm/themes/shadow-broker-theme.d.ts.map +1 -1
  199. package/esm/themes/shadow-broker-theme.js +1 -0
  200. package/esm/themes/shadow-broker-theme.js.map +1 -1
  201. package/esm/themes/sith-theme.d.ts +1 -0
  202. package/esm/themes/sith-theme.d.ts.map +1 -1
  203. package/esm/themes/sith-theme.js +1 -0
  204. package/esm/themes/sith-theme.js.map +1 -1
  205. package/esm/themes/vault-dweller-theme.d.ts +1 -0
  206. package/esm/themes/vault-dweller-theme.d.ts.map +1 -1
  207. package/esm/themes/vault-dweller-theme.js +1 -0
  208. package/esm/themes/vault-dweller-theme.js.map +1 -1
  209. package/esm/themes/wild-hunt-theme.d.ts +1 -0
  210. package/esm/themes/wild-hunt-theme.d.ts.map +1 -1
  211. package/esm/themes/wild-hunt-theme.js +1 -0
  212. package/esm/themes/wild-hunt-theme.js.map +1 -1
  213. package/esm/themes/xenomorph-theme.d.ts +1 -0
  214. package/esm/themes/xenomorph-theme.d.ts.map +1 -1
  215. package/esm/themes/xenomorph-theme.js +1 -0
  216. package/esm/themes/xenomorph-theme.js.map +1 -1
  217. package/package.json +7 -7
  218. package/src/components/accordion/accordion-item.tsx +9 -14
  219. package/src/components/accordion/accordion.spec.tsx +134 -79
  220. package/src/components/accordion/accordion.tsx +13 -1
  221. package/src/components/carousel.tsx +1 -1
  222. package/src/components/chip.spec.tsx +64 -0
  223. package/src/components/chip.tsx +4 -1
  224. package/src/components/command-palette/index.spec.tsx +95 -33
  225. package/src/components/command-palette/index.tsx +15 -3
  226. package/src/components/data-grid/data-grid-row.tsx +20 -2
  227. package/src/components/data-grid/data-grid.spec.tsx +185 -57
  228. package/src/components/data-grid/data-grid.tsx +38 -13
  229. package/src/components/data-grid/selection-cell.tsx +1 -0
  230. package/src/components/dialog.spec.tsx +77 -2
  231. package/src/components/dialog.tsx +14 -1
  232. package/src/components/dropdown.spec.tsx +9 -0
  233. package/src/components/dropdown.tsx +1 -0
  234. package/src/components/image.spec.tsx +82 -0
  235. package/src/components/image.tsx +16 -7
  236. package/src/components/inputs/checkbox.tsx +1 -0
  237. package/src/components/inputs/radio.tsx +1 -0
  238. package/src/components/inputs/slider.tsx +1 -0
  239. package/src/components/inputs/switch.tsx +1 -0
  240. package/src/components/list/list-item.tsx +22 -4
  241. package/src/components/list/list.spec.tsx +165 -32
  242. package/src/components/list/list.tsx +37 -10
  243. package/src/components/markdown/markdown-display.spec.tsx +132 -0
  244. package/src/components/markdown/markdown-display.tsx +12 -1
  245. package/src/components/markdown/markdown-editor.spec.tsx +123 -0
  246. package/src/components/menu/menu.tsx +1 -1
  247. package/src/components/modal.spec.tsx +124 -1
  248. package/src/components/modal.tsx +41 -3
  249. package/src/components/page-layout/index.spec.tsx +20 -0
  250. package/src/components/page-layout/index.tsx +1 -1
  251. package/src/components/rating.spec.tsx +199 -4
  252. package/src/components/rating.tsx +28 -22
  253. package/src/components/suggest/index.spec.tsx +147 -43
  254. package/src/components/suggest/index.tsx +15 -2
  255. package/src/components/suggest/suggest-manager.ts +2 -2
  256. package/src/components/tabs.tsx +4 -0
  257. package/src/components/tree/tree-item.tsx +19 -4
  258. package/src/components/tree/tree.spec.tsx +101 -2
  259. package/src/components/tree/tree.tsx +21 -3
  260. package/src/services/collection-service.spec.ts +33 -24
  261. package/src/services/collection-service.ts +35 -13
  262. package/src/services/css-variable-theme.spec.ts +1 -0
  263. package/src/services/css-variable-theme.ts +25 -0
  264. package/src/services/list-service.spec.ts +13 -42
  265. package/src/services/list-service.ts +15 -13
  266. package/src/services/theme-provider-service.ts +2 -0
  267. package/src/services/tree-service.spec.ts +12 -9
  268. package/src/services/tree-service.ts +5 -8
  269. package/src/themes/architect-theme.ts +1 -0
  270. package/src/themes/auditore-theme.ts +1 -0
  271. package/src/themes/black-mesa-theme.ts +1 -0
  272. package/src/themes/chieftain-theme.ts +1 -0
  273. package/src/themes/default-dark-theme.ts +1 -0
  274. package/src/themes/default-light-theme.ts +1 -0
  275. package/src/themes/dragonborn-theme.ts +1 -0
  276. package/src/themes/hawkins-theme.ts +1 -0
  277. package/src/themes/jedi-theme.ts +1 -0
  278. package/src/themes/neon-runner-theme.ts +1 -0
  279. package/src/themes/paladin-theme.ts +1 -0
  280. package/src/themes/plumber-theme.ts +1 -0
  281. package/src/themes/replicant-theme.ts +1 -0
  282. package/src/themes/sandworm-theme.ts +1 -0
  283. package/src/themes/shadow-broker-theme.ts +1 -0
  284. package/src/themes/sith-theme.ts +1 -0
  285. package/src/themes/vault-dweller-theme.ts +1 -0
  286. package/src/themes/wild-hunt-theme.ts +1 -0
  287. package/src/themes/xenomorph-theme.ts +1 -0
@@ -52,6 +52,34 @@ describe('Accordion', () => {
52
52
  await flushUpdates()
53
53
  expect(accordion.hasAttribute('data-variant')).toBe(false)
54
54
  })
55
+
56
+ describe('spatial navigation', () => {
57
+ it('should set data-nav-section with auto-generated id', async () => {
58
+ const el = (
59
+ <div>
60
+ <Accordion />
61
+ </div>
62
+ )
63
+ const accordion = el.firstElementChild as JSX.Element
64
+ accordion.updateComponent()
65
+ await flushUpdates()
66
+ const navSection = accordion.getAttribute('data-nav-section')
67
+ expect(navSection).toBeTruthy()
68
+ expect(navSection).toMatch(/^accordion-\d+$/)
69
+ })
70
+
71
+ it('should use custom navSection when provided', async () => {
72
+ const el = (
73
+ <div>
74
+ <Accordion navSection="my-accordion" />
75
+ </div>
76
+ )
77
+ const accordion = el.firstElementChild as JSX.Element
78
+ accordion.updateComponent()
79
+ await flushUpdates()
80
+ expect(accordion.getAttribute('data-nav-section')).toBe('my-accordion')
81
+ })
82
+ })
55
83
  })
56
84
 
57
85
  describe('AccordionItem', () => {
@@ -239,7 +267,7 @@ describe('AccordionItem', () => {
239
267
  expect(content.style.height).not.toBe('0px')
240
268
  })
241
269
 
242
- it('should have a role="button" on the header', async () => {
270
+ it('should render the header as a <button> element', async () => {
243
271
  const el = (
244
272
  <div>
245
273
  <AccordionItem title="Test" />
@@ -248,11 +276,12 @@ describe('AccordionItem', () => {
248
276
  const item = el.firstElementChild as JSX.Element
249
277
  item.updateComponent()
250
278
  await flushUpdates()
251
- const header = item.querySelector('.accordion-header') as HTMLElement
252
- expect(header.getAttribute('role')).toBe('button')
279
+ const header = item.querySelector('.accordion-header') as HTMLButtonElement
280
+ expect(header.tagName).toBe('BUTTON')
281
+ expect(header.type).toBe('button')
253
282
  })
254
283
 
255
- it('should set tabIndex on the header when not disabled', async () => {
284
+ it('should not disable the header button when not disabled', async () => {
256
285
  const el = (
257
286
  <div>
258
287
  <AccordionItem title="Test" />
@@ -261,11 +290,11 @@ describe('AccordionItem', () => {
261
290
  const item = el.firstElementChild as JSX.Element
262
291
  item.updateComponent()
263
292
  await flushUpdates()
264
- const header = item.querySelector('.accordion-header') as HTMLElement
265
- expect(header.tabIndex).toBe(0)
293
+ const header = item.querySelector('.accordion-header') as HTMLButtonElement
294
+ expect(header.disabled).toBe(false)
266
295
  })
267
296
 
268
- it('should set tabIndex to -1 on the header when disabled', async () => {
297
+ it('should disable the header button when disabled', async () => {
269
298
  const el = (
270
299
  <div>
271
300
  <AccordionItem title="Test" disabled />
@@ -274,8 +303,8 @@ describe('AccordionItem', () => {
274
303
  const item = el.firstElementChild as JSX.Element
275
304
  item.updateComponent()
276
305
  await flushUpdates()
277
- const header = item.querySelector('.accordion-header') as HTMLElement
278
- expect(header.tabIndex).toBe(-1)
306
+ const header = item.querySelector('.accordion-header') as HTMLButtonElement
307
+ expect(header.disabled).toBe(true)
279
308
  })
280
309
 
281
310
  it('should toggle data-expanded on header click', async () => {
@@ -299,7 +328,6 @@ describe('AccordionItem', () => {
299
328
  header.click()
300
329
  await flushUpdates()
301
330
 
302
- // The attribute is set synchronously before the animation awaits
303
331
  expect(item.hasAttribute('data-expanded')).toBe(true)
304
332
  })
305
333
 
@@ -324,75 +352,6 @@ describe('AccordionItem', () => {
324
352
  header.click()
325
353
  await flushUpdates()
326
354
 
327
- // The attribute is removed synchronously before the animation awaits
328
- expect(item.hasAttribute('data-expanded')).toBe(false)
329
- })
330
-
331
- it('should toggle on Enter keydown', async () => {
332
- const mockAnimation = { finished: Promise.resolve() }
333
- Element.prototype.animate = vi.fn().mockReturnValue(mockAnimation)
334
-
335
- const el = (
336
- <div>
337
- <AccordionItem title="Keyboard Toggle">
338
- <p>Content</p>
339
- </AccordionItem>
340
- </div>
341
- )
342
- const item = el.firstElementChild as JSX.Element
343
- item.updateComponent()
344
- await flushUpdates()
345
-
346
- expect(item.hasAttribute('data-expanded')).toBe(false)
347
-
348
- const header = item.querySelector('.accordion-header') as HTMLElement
349
- header.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
350
- await flushUpdates()
351
-
352
- expect(item.hasAttribute('data-expanded')).toBe(true)
353
- })
354
-
355
- it('should toggle on Space keydown', async () => {
356
- const mockAnimation = { finished: Promise.resolve() }
357
- Element.prototype.animate = vi.fn().mockReturnValue(mockAnimation)
358
-
359
- const el = (
360
- <div>
361
- <AccordionItem title="Space Toggle">
362
- <p>Content</p>
363
- </AccordionItem>
364
- </div>
365
- )
366
- const item = el.firstElementChild as JSX.Element
367
- item.updateComponent()
368
- await flushUpdates()
369
-
370
- expect(item.hasAttribute('data-expanded')).toBe(false)
371
-
372
- const header = item.querySelector('.accordion-header') as HTMLElement
373
- header.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }))
374
- await flushUpdates()
375
-
376
- expect(item.hasAttribute('data-expanded')).toBe(true)
377
- })
378
-
379
- it('should not toggle on unrelated keydown', async () => {
380
- const el = (
381
- <div>
382
- <AccordionItem title="No Toggle">
383
- <p>Content</p>
384
- </AccordionItem>
385
- </div>
386
- )
387
- const item = el.firstElementChild as JSX.Element
388
- item.updateComponent()
389
- await flushUpdates()
390
-
391
- expect(item.hasAttribute('data-expanded')).toBe(false)
392
-
393
- const header = item.querySelector('.accordion-header') as HTMLElement
394
- header.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
395
-
396
355
  expect(item.hasAttribute('data-expanded')).toBe(false)
397
356
  })
398
357
 
@@ -415,4 +374,100 @@ describe('AccordionItem', () => {
415
374
 
416
375
  expect(item.hasAttribute('data-expanded')).toBe(false)
417
376
  })
377
+
378
+ describe('spatial navigation integration', () => {
379
+ it('should set inert on content when collapsed', async () => {
380
+ const el = (
381
+ <div>
382
+ <AccordionItem title="Collapsed">
383
+ <p>Content</p>
384
+ </AccordionItem>
385
+ </div>
386
+ )
387
+ const item = el.firstElementChild as JSX.Element
388
+ item.updateComponent()
389
+ await flushUpdates()
390
+ const content = item.querySelector('.accordion-content') as HTMLElement
391
+ expect(content.inert).toBe(true)
392
+ })
393
+
394
+ it('should not set inert on content when expanded', async () => {
395
+ const el = (
396
+ <div>
397
+ <AccordionItem title="Expanded" defaultExpanded>
398
+ <p>Content</p>
399
+ </AccordionItem>
400
+ </div>
401
+ )
402
+ const item = el.firstElementChild as JSX.Element
403
+ item.updateComponent()
404
+ await flushUpdates()
405
+ const content = item.querySelector('.accordion-content') as HTMLElement
406
+ expect(content.inert).toBe(false)
407
+ })
408
+
409
+ it('should set inert on content after collapsing', async () => {
410
+ const mockAnimation = { finished: Promise.resolve() }
411
+ Element.prototype.animate = vi.fn().mockReturnValue(mockAnimation)
412
+
413
+ const el = (
414
+ <div>
415
+ <AccordionItem title="Toggleable" defaultExpanded>
416
+ <p>Content</p>
417
+ </AccordionItem>
418
+ </div>
419
+ )
420
+ const item = el.firstElementChild as JSX.Element
421
+ item.updateComponent()
422
+ await flushUpdates()
423
+
424
+ const content = item.querySelector('.accordion-content') as HTMLElement
425
+ expect(content.inert).toBe(false)
426
+
427
+ const header = item.querySelector('.accordion-header') as HTMLElement
428
+ header.click()
429
+ await flushUpdates()
430
+
431
+ expect(content.inert).toBe(true)
432
+ })
433
+
434
+ it('should remove inert on content after expanding', async () => {
435
+ const mockAnimation = { finished: Promise.resolve() }
436
+ Element.prototype.animate = vi.fn().mockReturnValue(mockAnimation)
437
+
438
+ const el = (
439
+ <div>
440
+ <AccordionItem title="Toggleable">
441
+ <p>Content</p>
442
+ </AccordionItem>
443
+ </div>
444
+ )
445
+ const item = el.firstElementChild as JSX.Element
446
+ item.updateComponent()
447
+ await flushUpdates()
448
+
449
+ const content = item.querySelector('.accordion-content') as HTMLElement
450
+ expect(content.inert).toBe(true)
451
+
452
+ const header = item.querySelector('.accordion-header') as HTMLElement
453
+ header.click()
454
+ await flushUpdates()
455
+
456
+ expect(content.inert).toBe(false)
457
+ })
458
+
459
+ it('should exclude disabled header from spatial navigation via native disabled attribute', async () => {
460
+ const el = (
461
+ <div>
462
+ <AccordionItem title="Disabled" disabled />
463
+ </div>
464
+ )
465
+ const item = el.firstElementChild as JSX.Element
466
+ item.updateComponent()
467
+ await flushUpdates()
468
+ const header = item.querySelector('.accordion-header') as HTMLButtonElement
469
+ expect(header.disabled).toBe(true)
470
+ expect(header.matches('button:not([disabled])')).toBe(false)
471
+ })
472
+ })
418
473
  })
@@ -1,12 +1,21 @@
1
1
  import { Shade, createComponent } from '@furystack/shades'
2
2
  import { cssVariableTheme } from '../../services/css-variable-theme.js'
3
3
 
4
+ let nextAccordionId = 0
5
+
4
6
  /**
5
7
  * Props for the Accordion container component.
6
8
  */
7
9
  export type AccordionProps = {
8
10
  /** Visual variant of the accordion container */
9
11
  variant?: 'outlined' | 'elevation'
12
+ /**
13
+ * Section name for spatial navigation scoping.
14
+ * Sets `data-nav-section` on the accordion so that SpatialNavigationService
15
+ * constrains arrow-key navigation within the accordion group.
16
+ * Auto-generated per instance when not provided.
17
+ */
18
+ navSection?: string
10
19
  }
11
20
 
12
21
  /**
@@ -41,9 +50,12 @@ export const Accordion = Shade<AccordionProps>({
41
50
  background: cssVariableTheme.background.paper,
42
51
  },
43
52
  },
44
- render: ({ props, useHostProps, children }) => {
53
+ render: ({ props, useHostProps, children, useState }) => {
54
+ const [navSectionId] = useState('navSectionId', String(nextAccordionId++))
55
+
45
56
  useHostProps({
46
57
  'data-variant': props.variant || undefined,
58
+ 'data-nav-section': props.navSection ?? `accordion-${navSectionId}`,
47
59
  })
48
60
 
49
61
  return <>{children}</>
@@ -200,7 +200,7 @@ export const Carousel = Shade<CarouselProps>({
200
200
  'data-vertical': vertical ? '' : undefined,
201
201
  role: 'region',
202
202
  'aria-roledescription': 'carousel',
203
- tabindex: '0',
203
+ tabIndex: 0,
204
204
  ...(style ? { style: style as Record<string, string> } : {}),
205
205
  })
206
206
 
@@ -140,6 +140,70 @@ describe('Chip', () => {
140
140
  expect(chip.hasAttribute('data-clickable')).toBe(true)
141
141
  })
142
142
 
143
+ it('should be focusable when clickable', async () => {
144
+ const el = (
145
+ <div>
146
+ <Chip clickable>Clickable</Chip>
147
+ </div>
148
+ )
149
+ const chip = el.firstElementChild as JSX.Element
150
+ chip.updateComponent()
151
+ await flushUpdates()
152
+ expect(chip.getAttribute('tabindex')).toBe('0')
153
+ })
154
+
155
+ it('should be focusable when onclick handler is provided', async () => {
156
+ const el = (
157
+ <div>
158
+ <Chip onclick={() => {}}>Click Handler</Chip>
159
+ </div>
160
+ )
161
+ const chip = el.firstElementChild as JSX.Element
162
+ chip.updateComponent()
163
+ await flushUpdates()
164
+ expect(chip.getAttribute('tabindex')).toBe('0')
165
+ })
166
+
167
+ it('should not be focusable when clickable but disabled', async () => {
168
+ const el = (
169
+ <div>
170
+ <Chip clickable disabled>
171
+ Disabled Clickable
172
+ </Chip>
173
+ </div>
174
+ )
175
+ const chip = el.firstElementChild as JSX.Element
176
+ chip.updateComponent()
177
+ await flushUpdates()
178
+ expect(chip.hasAttribute('tabindex')).toBe(false)
179
+ })
180
+
181
+ it('should not be focusable when not clickable', async () => {
182
+ const el = (
183
+ <div>
184
+ <Chip>Not Clickable</Chip>
185
+ </div>
186
+ )
187
+ const chip = el.firstElementChild as JSX.Element
188
+ chip.updateComponent()
189
+ await flushUpdates()
190
+ expect(chip.hasAttribute('tabindex')).toBe(false)
191
+ })
192
+
193
+ it('should have a focusable delete button', async () => {
194
+ const onDelete = vi.fn()
195
+ const el = (
196
+ <div>
197
+ <Chip onDelete={onDelete}>Deletable</Chip>
198
+ </div>
199
+ )
200
+ const chip = el.firstElementChild as JSX.Element
201
+ chip.updateComponent()
202
+ await flushUpdates()
203
+ const deleteBtn = chip.querySelector('.chip-delete') as HTMLElement
204
+ expect(deleteBtn.getAttribute('tabindex')).toBe('0')
205
+ })
206
+
143
207
  it('should set CSS custom properties for palette color', async () => {
144
208
  const el = (
145
209
  <div>
@@ -152,11 +152,13 @@ export const Chip = Shade<ChipProps>({
152
152
  const { variant, color, size, disabled, clickable, onDelete, style, ...rest } = props
153
153
 
154
154
  const colors = color ? paletteFullColors[color] : defaultColors
155
+ const isClickable = clickable || rest.onclick
155
156
  useHostProps({
156
157
  'data-variant': variant || undefined,
157
158
  'data-size': size === 'small' ? 'small' : undefined,
158
159
  'data-disabled': disabled ? '' : undefined,
159
- 'data-clickable': clickable || rest.onclick ? '' : undefined,
160
+ 'data-clickable': isClickable ? '' : undefined,
161
+ tabIndex: isClickable && !disabled ? 0 : undefined,
160
162
  style: {
161
163
  '--chip-color-main': colors.main,
162
164
  '--chip-color-main-contrast': colors.mainContrast,
@@ -174,6 +176,7 @@ export const Chip = Shade<ChipProps>({
174
176
  <span
175
177
  className="chip-delete"
176
178
  role="button"
179
+ tabIndex={0}
177
180
  onclick={(ev: MouseEvent) => {
178
181
  ev.stopPropagation()
179
182
  onDelete(ev)
@@ -121,12 +121,16 @@ describe('CommandPalette', () => {
121
121
  })
122
122
 
123
123
  describe('keyboard navigation', () => {
124
- const triggerKeyup = (input: HTMLInputElement, key: string) => {
125
- const event = new KeyboardEvent('keyup', { key, bubbles: true })
124
+ const triggerKeydown = (input: HTMLInputElement, key: string) => {
125
+ const event = new KeyboardEvent('keydown', { key, bubbles: true })
126
126
  Object.defineProperty(event, 'target', { value: input, writable: false })
127
127
  input.dispatchEvent(event)
128
128
  }
129
129
 
130
+ const triggerInput = (input: HTMLInputElement) => {
131
+ input.dispatchEvent(new Event('input', { bubbles: true }))
132
+ }
133
+
130
134
  const getSuggestionItems = (commandPalette: HTMLElement) => {
131
135
  const suggestionList = commandPalette.querySelector('shade-command-palette-suggestion-list') as HTMLElement
132
136
  return suggestionList?.querySelectorAll('.suggestion-item') || []
@@ -155,13 +159,13 @@ describe('CommandPalette', () => {
155
159
 
156
160
  // Open and trigger suggestions
157
161
  input.value = 'test'
158
- triggerKeyup(input, 'a')
162
+ triggerInput(input)
159
163
 
160
164
  await vi.advanceTimersByTimeAsync(300)
161
165
  await flushUpdates()
162
166
 
163
167
  // Press ArrowDown
164
- triggerKeyup(input, 'ArrowDown')
168
+ triggerKeydown(input, 'ArrowDown')
165
169
  await flushUpdates()
166
170
 
167
171
  const suggestionItems = getSuggestionItems(commandPalette)
@@ -192,15 +196,15 @@ describe('CommandPalette', () => {
192
196
 
193
197
  // Open and trigger suggestions
194
198
  input.value = 'test'
195
- triggerKeyup(input, 'a')
199
+ triggerInput(input)
196
200
 
197
201
  await vi.advanceTimersByTimeAsync(300)
198
202
  await flushUpdates()
199
203
 
200
204
  // Navigate down then up
201
- triggerKeyup(input, 'ArrowDown')
205
+ triggerKeydown(input, 'ArrowDown')
202
206
  await flushUpdates()
203
- triggerKeyup(input, 'ArrowUp')
207
+ triggerKeydown(input, 'ArrowUp')
204
208
  await flushUpdates()
205
209
 
206
210
  const suggestionItems = getSuggestionItems(commandPalette)
@@ -226,15 +230,15 @@ describe('CommandPalette', () => {
226
230
  const input = commandPalette.querySelector('input') as HTMLInputElement
227
231
 
228
232
  input.value = 'test'
229
- triggerKeyup(input, 'a')
233
+ triggerInput(input)
230
234
 
231
235
  await vi.advanceTimersByTimeAsync(300)
232
236
  await flushUpdates()
233
237
 
234
238
  // Press ArrowDown multiple times
235
- triggerKeyup(input, 'ArrowDown')
236
- triggerKeyup(input, 'ArrowDown')
237
- triggerKeyup(input, 'ArrowDown')
239
+ triggerKeydown(input, 'ArrowDown')
240
+ triggerKeydown(input, 'ArrowDown')
241
+ triggerKeydown(input, 'ArrowDown')
238
242
  await flushUpdates()
239
243
 
240
244
  const suggestionItems = getSuggestionItems(commandPalette)
@@ -260,13 +264,13 @@ describe('CommandPalette', () => {
260
264
  const input = commandPalette.querySelector('input') as HTMLInputElement
261
265
 
262
266
  input.value = 'test'
263
- triggerKeyup(input, 'a')
267
+ triggerInput(input)
264
268
 
265
269
  await vi.advanceTimersByTimeAsync(300)
266
270
  await flushUpdates()
267
271
 
268
272
  // Press ArrowUp when already at first item
269
- triggerKeyup(input, 'ArrowUp')
273
+ triggerKeydown(input, 'ArrowUp')
270
274
  await flushUpdates()
271
275
 
272
276
  const suggestionItems = getSuggestionItems(commandPalette)
@@ -293,13 +297,13 @@ describe('CommandPalette', () => {
293
297
  const input = commandPalette.querySelector('input') as HTMLInputElement
294
298
 
295
299
  input.value = 'test'
296
- triggerKeyup(input, 'a')
300
+ triggerInput(input)
297
301
 
298
302
  await vi.advanceTimersByTimeAsync(300)
299
303
  await flushUpdates()
300
304
 
301
305
  // Press Enter
302
- triggerKeyup(input, 'Enter')
306
+ triggerKeydown(input, 'Enter')
303
307
  await flushUpdates()
304
308
 
305
309
  expect(onSelected).toHaveBeenCalledTimes(1)
@@ -309,12 +313,16 @@ describe('CommandPalette', () => {
309
313
  })
310
314
 
311
315
  describe('selection', () => {
312
- const triggerKeyup = (input: HTMLInputElement, key: string) => {
313
- const event = new KeyboardEvent('keyup', { key, bubbles: true })
316
+ const triggerKeydown = (input: HTMLInputElement, key: string) => {
317
+ const event = new KeyboardEvent('keydown', { key, bubbles: true })
314
318
  Object.defineProperty(event, 'target', { value: input, writable: false })
315
319
  input.dispatchEvent(event)
316
320
  }
317
321
 
322
+ const triggerInput = (input: HTMLInputElement) => {
323
+ input.dispatchEvent(new Event('input', { bubbles: true }))
324
+ }
325
+
318
326
  const getSuggestionItems = (commandPalette: HTMLElement) => {
319
327
  const suggestionList = commandPalette.querySelector('shade-command-palette-suggestion-list') as HTMLElement
320
328
  return suggestionList?.querySelectorAll('.suggestion-item') || []
@@ -345,7 +353,7 @@ describe('CommandPalette', () => {
345
353
 
346
354
  const input = commandPalette.querySelector('input') as HTMLInputElement
347
355
  input.value = 'test'
348
- triggerKeyup(input, 'a')
356
+ triggerInput(input)
349
357
 
350
358
  await vi.advanceTimersByTimeAsync(300)
351
359
  await flushUpdates()
@@ -386,12 +394,13 @@ describe('CommandPalette', () => {
386
394
 
387
395
  const input = commandPalette.querySelector('input') as HTMLInputElement
388
396
  input.value = 'test'
389
- triggerKeyup(input, 'a')
397
+ triggerInput(input)
390
398
 
399
+ await vi.advanceTimersByTimeAsync(300)
391
400
  await flushUpdates()
392
401
 
393
402
  // Select via Enter
394
- triggerKeyup(input, 'Enter')
403
+ triggerKeydown(input, 'Enter')
395
404
  await flushUpdates()
396
405
 
397
406
  expect(commandPalette.hasAttribute('data-opened')).toBe(false)
@@ -400,10 +409,8 @@ describe('CommandPalette', () => {
400
409
  })
401
410
 
402
411
  describe('command providers', () => {
403
- const triggerKeyup = (input: HTMLInputElement, key: string) => {
404
- const event = new KeyboardEvent('keyup', { key, bubbles: true })
405
- Object.defineProperty(event, 'target', { value: input, writable: false })
406
- input.dispatchEvent(event)
412
+ const triggerInput = (input: HTMLInputElement) => {
413
+ input.dispatchEvent(new Event('input', { bubbles: true }))
407
414
  }
408
415
 
409
416
  const getSuggestionItems = (commandPalette: HTMLElement) => {
@@ -430,7 +437,7 @@ describe('CommandPalette', () => {
430
437
  const input = commandPalette.querySelector('input') as HTMLInputElement
431
438
 
432
439
  input.value = 'search'
433
- triggerKeyup(input, 'h')
440
+ triggerInput(input)
434
441
 
435
442
  await vi.advanceTimersByTimeAsync(300)
436
443
  await flushUpdates()
@@ -459,7 +466,7 @@ describe('CommandPalette', () => {
459
466
  const input = commandPalette.querySelector('input') as HTMLInputElement
460
467
 
461
468
  input.value = 'search'
462
- triggerKeyup(input, 'h')
469
+ triggerInput(input)
463
470
 
464
471
  await vi.advanceTimersByTimeAsync(300)
465
472
  await flushUpdates()
@@ -493,7 +500,7 @@ describe('CommandPalette', () => {
493
500
  const input = commandPalette.querySelector('input') as HTMLInputElement
494
501
 
495
502
  input.value = 'search'
496
- triggerKeyup(input, 'h')
503
+ triggerInput(input)
497
504
 
498
505
  await vi.advanceTimersByTimeAsync(300)
499
506
  await flushUpdates()
@@ -585,9 +592,7 @@ describe('CommandPalette', () => {
585
592
  const input = commandPalette.querySelector('input') as HTMLInputElement
586
593
 
587
594
  input.value = 'test'
588
- const event = new KeyboardEvent('keyup', { key: 't', bubbles: true })
589
- Object.defineProperty(event, 'target', { value: input, writable: false })
590
- input.dispatchEvent(event)
595
+ input.dispatchEvent(new Event('input', { bubbles: true }))
591
596
 
592
597
  await vi.advanceTimersByTimeAsync(260)
593
598
  await flushUpdates()
@@ -602,6 +607,65 @@ describe('CommandPalette', () => {
602
607
  })
603
608
  })
604
609
 
610
+ describe('spatial navigation attributes', () => {
611
+ it('should have data-spatial-nav-target on the host element', async () => {
612
+ await usingAsync(new Injector(), async (injector) => {
613
+ const rootElement = document.getElementById('root') as HTMLDivElement
614
+
615
+ initializeShadeRoot({
616
+ injector,
617
+ rootElement,
618
+ jsxElement: <CommandPalette commandProviders={[]} defaultPrefix=">" />,
619
+ })
620
+
621
+ await flushUpdates()
622
+
623
+ const commandPalette = document.querySelector('shade-command-palette') as HTMLElement
624
+ expect(commandPalette.hasAttribute('data-spatial-nav-target')).toBe(true)
625
+ })
626
+ })
627
+
628
+ it('should have tabIndex of -1 on the host element', async () => {
629
+ await usingAsync(new Injector(), async (injector) => {
630
+ const rootElement = document.getElementById('root') as HTMLDivElement
631
+
632
+ initializeShadeRoot({
633
+ injector,
634
+ rootElement,
635
+ jsxElement: <CommandPalette commandProviders={[]} defaultPrefix=">" />,
636
+ })
637
+
638
+ await flushUpdates()
639
+
640
+ const commandPalette = document.querySelector('shade-command-palette') as HTMLElement
641
+ expect(commandPalette.tabIndex).toBe(-1)
642
+ })
643
+ })
644
+
645
+ it('should delegate focus to the inner input when the host is focused', async () => {
646
+ await usingAsync(new Injector(), async (injector) => {
647
+ const rootElement = document.getElementById('root') as HTMLDivElement
648
+
649
+ initializeShadeRoot({
650
+ injector,
651
+ rootElement,
652
+ jsxElement: <CommandPalette commandProviders={[]} defaultPrefix=">" />,
653
+ })
654
+
655
+ await flushUpdates()
656
+
657
+ const commandPalette = document.querySelector('shade-command-palette') as HTMLElement
658
+ const input = commandPalette.querySelector('input') as HTMLInputElement
659
+
660
+ commandPalette.dispatchEvent(new FocusEvent('focus', { bubbles: false }))
661
+
662
+ await flushUpdates()
663
+
664
+ expect(document.activeElement).toBe(input)
665
+ })
666
+ })
667
+ })
668
+
605
669
  describe('click away', () => {
606
670
  it('should close when clicking outside the component', async () => {
607
671
  await usingAsync(new Injector(), async (injector) => {
@@ -681,9 +745,7 @@ describe('CommandPalette', () => {
681
745
 
682
746
  const input = commandPalette.querySelector('input') as HTMLInputElement
683
747
  input.value = 'test'
684
- const event = new KeyboardEvent('keyup', { key: 't', bubbles: true })
685
- Object.defineProperty(event, 'target', { value: input, writable: false })
686
- input.dispatchEvent(event)
748
+ input.dispatchEvent(new Event('input', { bubbles: true }))
687
749
 
688
750
  await flushUpdates()
689
751