@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
@@ -588,7 +588,7 @@ describe('Rating', () => {
588
588
  })
589
589
  })
590
590
 
591
- it('should increase value with ArrowUp', async () => {
591
+ it('should not change value with ArrowUp (reserved for spatial navigation)', async () => {
592
592
  await usingAsync(new Injector(), async (injector) => {
593
593
  const rootElement = document.getElementById('root') as HTMLDivElement
594
594
  const onchange = vi.fn()
@@ -606,11 +606,11 @@ describe('Rating', () => {
606
606
 
607
607
  await flushUpdates()
608
608
 
609
- expect(onchange).toHaveBeenCalledWith(3)
609
+ expect(onchange).not.toHaveBeenCalled()
610
610
  })
611
611
  })
612
612
 
613
- it('should decrease value with ArrowDown', async () => {
613
+ it('should not change value with ArrowDown (reserved for spatial navigation)', async () => {
614
614
  await usingAsync(new Injector(), async (injector) => {
615
615
  const rootElement = document.getElementById('root') as HTMLDivElement
616
616
  const onchange = vi.fn()
@@ -628,7 +628,7 @@ describe('Rating', () => {
628
628
 
629
629
  await flushUpdates()
630
630
 
631
- expect(onchange).toHaveBeenCalledWith(2)
631
+ expect(onchange).not.toHaveBeenCalled()
632
632
  })
633
633
  })
634
634
 
@@ -863,4 +863,199 @@ describe('Rating', () => {
863
863
  })
864
864
  })
865
865
  })
866
+
867
+ describe('spatial navigation integration', () => {
868
+ it('should set data-spatial-nav-target on the host element', async () => {
869
+ await usingAsync(new Injector(), async (injector) => {
870
+ const rootElement = document.getElementById('root') as HTMLDivElement
871
+
872
+ initializeShadeRoot({
873
+ injector,
874
+ rootElement,
875
+ jsxElement: <Rating value={3} />,
876
+ })
877
+
878
+ await flushUpdates()
879
+
880
+ const wrapper = document.querySelector('shade-rating') as HTMLElement
881
+ expect(wrapper.hasAttribute('data-spatial-nav-target')).toBe(true)
882
+ })
883
+ })
884
+
885
+ it('should not set data-spatial-nav-target when readOnly', async () => {
886
+ await usingAsync(new Injector(), async (injector) => {
887
+ const rootElement = document.getElementById('root') as HTMLDivElement
888
+
889
+ initializeShadeRoot({
890
+ injector,
891
+ rootElement,
892
+ jsxElement: <Rating value={3} readOnly />,
893
+ })
894
+
895
+ await flushUpdates()
896
+
897
+ const wrapper = document.querySelector('shade-rating') as HTMLElement
898
+ expect(wrapper.hasAttribute('data-spatial-nav-target')).toBe(false)
899
+ })
900
+ })
901
+
902
+ it('should not set data-spatial-nav-target when disabled', async () => {
903
+ await usingAsync(new Injector(), async (injector) => {
904
+ const rootElement = document.getElementById('root') as HTMLDivElement
905
+
906
+ initializeShadeRoot({
907
+ injector,
908
+ rootElement,
909
+ jsxElement: <Rating value={3} disabled />,
910
+ })
911
+
912
+ await flushUpdates()
913
+
914
+ const wrapper = document.querySelector('shade-rating') as HTMLElement
915
+ expect(wrapper.hasAttribute('data-spatial-nav-target')).toBe(false)
916
+ })
917
+ })
918
+
919
+ it('should set aria-orientation to horizontal', async () => {
920
+ await usingAsync(new Injector(), async (injector) => {
921
+ const rootElement = document.getElementById('root') as HTMLDivElement
922
+
923
+ initializeShadeRoot({
924
+ injector,
925
+ rootElement,
926
+ jsxElement: <Rating value={3} />,
927
+ })
928
+
929
+ await flushUpdates()
930
+
931
+ const wrapper = document.querySelector('shade-rating') as HTMLElement
932
+ expect(wrapper.getAttribute('aria-orientation')).toBe('horizontal')
933
+ })
934
+ })
935
+
936
+ it('should not preventDefault on ArrowRight when at max value', async () => {
937
+ await usingAsync(new Injector(), async (injector) => {
938
+ const rootElement = document.getElementById('root') as HTMLDivElement
939
+ const onchange = vi.fn()
940
+
941
+ initializeShadeRoot({
942
+ injector,
943
+ rootElement,
944
+ jsxElement: <Rating value={5} max={5} onValueChange={onchange} />,
945
+ })
946
+
947
+ await flushUpdates()
948
+
949
+ const ratingEl = document.querySelector('shade-rating') as HTMLElement
950
+ const event = new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, cancelable: true })
951
+ ratingEl.dispatchEvent(event)
952
+
953
+ await flushUpdates()
954
+
955
+ expect(onchange).not.toHaveBeenCalled()
956
+ expect(event.defaultPrevented).toBe(false)
957
+ })
958
+ })
959
+
960
+ it('should not preventDefault on ArrowLeft when at min value', async () => {
961
+ await usingAsync(new Injector(), async (injector) => {
962
+ const rootElement = document.getElementById('root') as HTMLDivElement
963
+ const onchange = vi.fn()
964
+
965
+ initializeShadeRoot({
966
+ injector,
967
+ rootElement,
968
+ jsxElement: <Rating value={0} onValueChange={onchange} />,
969
+ })
970
+
971
+ await flushUpdates()
972
+
973
+ const ratingEl = document.querySelector('shade-rating') as HTMLElement
974
+ const event = new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true, cancelable: true })
975
+ ratingEl.dispatchEvent(event)
976
+
977
+ await flushUpdates()
978
+
979
+ expect(onchange).not.toHaveBeenCalled()
980
+ expect(event.defaultPrevented).toBe(false)
981
+ })
982
+ })
983
+
984
+ it('should preventDefault on ArrowRight when value can increase', async () => {
985
+ await usingAsync(new Injector(), async (injector) => {
986
+ const rootElement = document.getElementById('root') as HTMLDivElement
987
+ const onchange = vi.fn()
988
+
989
+ initializeShadeRoot({
990
+ injector,
991
+ rootElement,
992
+ jsxElement: <Rating value={3} max={5} onValueChange={onchange} />,
993
+ })
994
+
995
+ await flushUpdates()
996
+
997
+ const ratingEl = document.querySelector('shade-rating') as HTMLElement
998
+ const event = new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, cancelable: true })
999
+ ratingEl.dispatchEvent(event)
1000
+
1001
+ await flushUpdates()
1002
+
1003
+ expect(onchange).toHaveBeenCalledWith(4)
1004
+ expect(event.defaultPrevented).toBe(true)
1005
+ })
1006
+ })
1007
+
1008
+ it('should preventDefault on ArrowLeft when value can decrease', async () => {
1009
+ await usingAsync(new Injector(), async (injector) => {
1010
+ const rootElement = document.getElementById('root') as HTMLDivElement
1011
+ const onchange = vi.fn()
1012
+
1013
+ initializeShadeRoot({
1014
+ injector,
1015
+ rootElement,
1016
+ jsxElement: <Rating value={3} onValueChange={onchange} />,
1017
+ })
1018
+
1019
+ await flushUpdates()
1020
+
1021
+ const ratingEl = document.querySelector('shade-rating') as HTMLElement
1022
+ const event = new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true, cancelable: true })
1023
+ ratingEl.dispatchEvent(event)
1024
+
1025
+ await flushUpdates()
1026
+
1027
+ expect(onchange).toHaveBeenCalledWith(2)
1028
+ expect(event.defaultPrevented).toBe(true)
1029
+ })
1030
+ })
1031
+
1032
+ it('should not preventDefault on ArrowUp or ArrowDown', async () => {
1033
+ await usingAsync(new Injector(), async (injector) => {
1034
+ const rootElement = document.getElementById('root') as HTMLDivElement
1035
+ const onchange = vi.fn()
1036
+
1037
+ initializeShadeRoot({
1038
+ injector,
1039
+ rootElement,
1040
+ jsxElement: <Rating value={3} onValueChange={onchange} />,
1041
+ })
1042
+
1043
+ await flushUpdates()
1044
+
1045
+ const ratingEl = document.querySelector('shade-rating') as HTMLElement
1046
+
1047
+ const upEvent = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true, cancelable: true })
1048
+ ratingEl.dispatchEvent(upEvent)
1049
+
1050
+ const downEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true })
1051
+ ratingEl.dispatchEvent(downEvent)
1052
+
1053
+ await flushUpdates()
1054
+
1055
+ expect(onchange).not.toHaveBeenCalled()
1056
+ expect(upEvent.defaultPrevented).toBe(false)
1057
+ expect(downEvent.defaultPrevented).toBe(false)
1058
+ })
1059
+ })
1060
+ })
866
1061
  })
@@ -114,8 +114,7 @@ export const Rating = Shade<RatingProps>({
114
114
 
115
115
  '&:focus-visible': {
116
116
  outline: 'none',
117
- boxShadow: cssVariableTheme.action.focusRing,
118
- borderRadius: cssVariableTheme.shape.borderRadius.xs,
117
+ boxShadow: `0 0 0 2px ${cssVariableTheme.palette.primary.main} inset`,
119
118
  },
120
119
 
121
120
  '&[data-size="small"] .rating-star': {
@@ -159,46 +158,53 @@ export const Rating = Shade<RatingProps>({
159
158
  useHostProps({
160
159
  'data-size': props.size || 'medium',
161
160
  style: { '--rating-color': color },
161
+ ...(isInteractive ? { 'data-spatial-nav-target': '' } : {}),
162
162
  ...(props.disabled ? { 'data-disabled': '', 'aria-disabled': 'true' } : {}),
163
163
  ...(props.readOnly ? { 'data-readonly': '', 'aria-readonly': 'true' } : {}),
164
164
  ...(isInteractive
165
165
  ? {
166
166
  role: 'slider',
167
- tabindex: '0',
167
+ tabIndex: 0,
168
168
  'aria-valuenow': String(value),
169
169
  'aria-valuemin': '0',
170
170
  'aria-valuemax': String(max),
171
171
  'aria-label': 'Rating',
172
+ 'aria-orientation': 'horizontal',
172
173
  onkeydown: (ev: KeyboardEvent) => {
173
174
  const step = precision === 0.5 ? 0.5 : 1
174
- let newValue: number
175
175
 
176
176
  switch (ev.key) {
177
- case 'ArrowRight':
178
- case 'ArrowUp':
179
- ev.preventDefault()
180
- newValue = Math.min(value + step, max)
181
- break
182
- case 'ArrowLeft':
183
- case 'ArrowDown':
184
- ev.preventDefault()
185
- newValue = Math.max(value - step, 0)
186
- break
177
+ case 'ArrowRight': {
178
+ const newValue = Math.min(value + step, max)
179
+ if (newValue !== value) {
180
+ ev.preventDefault()
181
+ props.onValueChange?.(newValue)
182
+ }
183
+ return
184
+ }
185
+ case 'ArrowLeft': {
186
+ const newValue = Math.max(value - step, 0)
187
+ if (newValue !== value) {
188
+ ev.preventDefault()
189
+ props.onValueChange?.(newValue)
190
+ }
191
+ return
192
+ }
187
193
  case 'Home':
188
194
  ev.preventDefault()
189
- newValue = 0
190
- break
195
+ if (value !== 0) {
196
+ props.onValueChange?.(0)
197
+ }
198
+ return
191
199
  case 'End':
192
200
  ev.preventDefault()
193
- newValue = max
194
- break
201
+ if (value !== max) {
202
+ props.onValueChange?.(max)
203
+ }
204
+ return
195
205
  default:
196
206
  return
197
207
  }
198
-
199
- if (newValue !== value) {
200
- props.onValueChange?.(newValue)
201
- }
202
208
  },
203
209
  }
204
210
  : {
@@ -200,13 +200,11 @@ describe('Suggest', () => {
200
200
  const input = suggest?.querySelector('input') as HTMLInputElement
201
201
  input.value = 'test'
202
202
 
203
- const keyupEvent = new KeyboardEvent('keyup', { key: 'a', bubbles: true })
204
- Object.defineProperty(keyupEvent, 'target', { value: input })
205
- wrapper?.dispatchEvent(keyupEvent)
203
+ input.dispatchEvent(new Event('input', { bubbles: true }))
206
204
 
207
205
  await advanceTimers(300)
208
206
 
209
- const arrowDownEvent = new KeyboardEvent('keyup', { key: 'ArrowDown', bubbles: true })
207
+ const arrowDownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
210
208
  Object.defineProperty(arrowDownEvent, 'target', { value: input })
211
209
  wrapper?.dispatchEvent(arrowDownEvent)
212
210
 
@@ -243,18 +241,16 @@ describe('Suggest', () => {
243
241
  const input = suggest?.querySelector('input') as HTMLInputElement
244
242
  input.value = 'test'
245
243
 
246
- const keyupEvent = new KeyboardEvent('keyup', { key: 'a', bubbles: true })
247
- Object.defineProperty(keyupEvent, 'target', { value: input })
248
- wrapper?.dispatchEvent(keyupEvent)
244
+ input.dispatchEvent(new Event('input', { bubbles: true }))
249
245
 
250
246
  await advanceTimers(300)
251
247
 
252
- const arrowDownEvent = new KeyboardEvent('keyup', { key: 'ArrowDown', bubbles: true })
248
+ const arrowDownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
253
249
  Object.defineProperty(arrowDownEvent, 'target', { value: input })
254
250
  wrapper?.dispatchEvent(arrowDownEvent)
255
251
  wrapper?.dispatchEvent(arrowDownEvent)
256
252
 
257
- const arrowUpEvent = new KeyboardEvent('keyup', { key: 'ArrowUp', bubbles: true })
253
+ const arrowUpEvent = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
258
254
  Object.defineProperty(arrowUpEvent, 'target', { value: input })
259
255
  wrapper?.dispatchEvent(arrowUpEvent)
260
256
 
@@ -290,13 +286,11 @@ describe('Suggest', () => {
290
286
  const input = suggest?.querySelector('input') as HTMLInputElement
291
287
  input.value = 'First'
292
288
 
293
- const keyupEvent = new KeyboardEvent('keyup', { key: 'a', bubbles: true })
294
- Object.defineProperty(keyupEvent, 'target', { value: input })
295
- wrapper?.dispatchEvent(keyupEvent)
289
+ input.dispatchEvent(new Event('input', { bubbles: true }))
296
290
 
297
291
  await advanceTimers(300)
298
292
 
299
- const enterEvent = new KeyboardEvent('keyup', { key: 'Enter', bubbles: true })
293
+ const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
300
294
  Object.defineProperty(enterEvent, 'target', { value: input })
301
295
  wrapper?.dispatchEvent(enterEvent)
302
296
 
@@ -332,13 +326,11 @@ describe('Suggest', () => {
332
326
  const input = suggest?.querySelector('input') as HTMLInputElement
333
327
  input.value = 'First'
334
328
 
335
- const keyupEvent = new KeyboardEvent('keyup', { key: 'a', bubbles: true })
336
- Object.defineProperty(keyupEvent, 'target', { value: input })
337
- wrapper?.dispatchEvent(keyupEvent)
329
+ input.dispatchEvent(new Event('input', { bubbles: true }))
338
330
 
339
331
  await advanceTimers(300)
340
332
 
341
- const enterEvent = new KeyboardEvent('keyup', { key: 'Enter', bubbles: true, cancelable: true })
333
+ const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })
342
334
  Object.defineProperty(enterEvent, 'target', { value: input })
343
335
 
344
336
  const preventDefaultSpy = vi.spyOn(enterEvent, 'preventDefault')
@@ -348,7 +340,7 @@ describe('Suggest', () => {
348
340
  })
349
341
  })
350
342
 
351
- it('should prevent default on ArrowUp key', async () => {
343
+ it('should prevent default on ArrowUp key when suggestions are open', async () => {
352
344
  await usingAsync(new Injector(), async (injector) => {
353
345
  const rootElement = document.getElementById('root') as HTMLDivElement
354
346
  const onSelectSuggestion = vi.fn()
@@ -372,8 +364,11 @@ describe('Suggest', () => {
372
364
  const wrapper = suggest?.querySelector('.suggest-wrapper') as HTMLElement
373
365
 
374
366
  const input = suggest?.querySelector('input') as HTMLInputElement
367
+ input.value = 'First'
368
+ input.dispatchEvent(new Event('input', { bubbles: true }))
369
+ await advanceTimers(300)
375
370
 
376
- const arrowUpEvent = new KeyboardEvent('keyup', { key: 'ArrowUp', bubbles: true, cancelable: true })
371
+ const arrowUpEvent = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true, cancelable: true })
377
372
  Object.defineProperty(arrowUpEvent, 'target', { value: input })
378
373
 
379
374
  const preventDefaultSpy = vi.spyOn(arrowUpEvent, 'preventDefault')
@@ -383,7 +378,7 @@ describe('Suggest', () => {
383
378
  })
384
379
  })
385
380
 
386
- it('should prevent default on ArrowDown key', async () => {
381
+ it('should prevent default on ArrowDown key when suggestions are open', async () => {
387
382
  await usingAsync(new Injector(), async (injector) => {
388
383
  const rootElement = document.getElementById('root') as HTMLDivElement
389
384
  const onSelectSuggestion = vi.fn()
@@ -407,8 +402,11 @@ describe('Suggest', () => {
407
402
  const wrapper = suggest?.querySelector('.suggest-wrapper') as HTMLElement
408
403
 
409
404
  const input = suggest?.querySelector('input') as HTMLInputElement
405
+ input.value = 'First'
406
+ input.dispatchEvent(new Event('input', { bubbles: true }))
407
+ await advanceTimers(300)
410
408
 
411
- const arrowDownEvent = new KeyboardEvent('keyup', { key: 'ArrowDown', bubbles: true, cancelable: true })
409
+ const arrowDownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true })
412
410
  Object.defineProperty(arrowDownEvent, 'target', { value: input })
413
411
 
414
412
  const preventDefaultSpy = vi.spyOn(arrowDownEvent, 'preventDefault')
@@ -418,6 +416,39 @@ describe('Suggest', () => {
418
416
  })
419
417
  })
420
418
 
419
+ it('should not prevent default on arrow keys when dropdown is closed', async () => {
420
+ await usingAsync(new Injector(), async (injector) => {
421
+ const rootElement = document.getElementById('root') as HTMLDivElement
422
+ const onSelectSuggestion = vi.fn()
423
+
424
+ initializeShadeRoot({
425
+ injector,
426
+ rootElement,
427
+ jsxElement: (
428
+ <Suggest<TestEntry>
429
+ defaultPrefix="🔍"
430
+ getEntries={getTestEntries}
431
+ getSuggestionEntry={getSuggestionEntry}
432
+ onSelectSuggestion={onSelectSuggestion}
433
+ />
434
+ ),
435
+ })
436
+
437
+ await advanceTimers(50)
438
+
439
+ const suggest = document.querySelector('shade-suggest') as HTMLElement
440
+ const wrapper = suggest?.querySelector('.suggest-wrapper') as HTMLElement
441
+ const input = suggest?.querySelector('input') as HTMLInputElement
442
+
443
+ const arrowDownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true })
444
+ Object.defineProperty(arrowDownEvent, 'target', { value: input })
445
+ const preventDefaultSpy = vi.spyOn(arrowDownEvent, 'preventDefault')
446
+ wrapper?.dispatchEvent(arrowDownEvent)
447
+
448
+ expect(preventDefaultSpy).not.toHaveBeenCalled()
449
+ })
450
+ })
451
+
421
452
  it('should not move selection below 0', async () => {
422
453
  await usingAsync(new Injector(), async (injector) => {
423
454
  const rootElement = document.getElementById('root') as HTMLDivElement
@@ -442,9 +473,12 @@ describe('Suggest', () => {
442
473
  const wrapper = suggest?.querySelector('.suggest-wrapper') as HTMLElement
443
474
 
444
475
  const input = suggest?.querySelector('input') as HTMLInputElement
476
+ input.value = 'test'
477
+ input.dispatchEvent(new Event('input', { bubbles: true }))
478
+ await advanceTimers(300)
445
479
 
446
480
  for (let i = 0; i < 5; i++) {
447
- const arrowUpEvent = new KeyboardEvent('keyup', { key: 'ArrowUp', bubbles: true })
481
+ const arrowUpEvent = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
448
482
  Object.defineProperty(arrowUpEvent, 'target', { value: input })
449
483
  wrapper?.dispatchEvent(arrowUpEvent)
450
484
  }
@@ -616,14 +650,11 @@ describe('Suggest', () => {
616
650
  await advanceTimers(50)
617
651
 
618
652
  const suggest = document.querySelector('shade-suggest') as HTMLElement
619
- const wrapper = suggest?.querySelector('.suggest-wrapper') as HTMLElement
620
653
 
621
654
  const input = suggest?.querySelector('input') as HTMLInputElement
622
655
  input.value = 'First'
623
656
 
624
- const keyupEvent = new KeyboardEvent('keyup', { key: 'a', bubbles: true })
625
- Object.defineProperty(keyupEvent, 'target', { value: input })
626
- wrapper?.dispatchEvent(keyupEvent)
657
+ input.dispatchEvent(new Event('input', { bubbles: true }))
627
658
 
628
659
  await advanceTimers(300)
629
660
 
@@ -658,14 +689,11 @@ describe('Suggest', () => {
658
689
  await advanceTimers(50)
659
690
 
660
691
  const suggest = document.querySelector('shade-suggest') as HTMLElement
661
- const wrapper = suggest?.querySelector('.suggest-wrapper') as HTMLElement
662
692
 
663
693
  const input = suggest?.querySelector('input') as HTMLInputElement
664
694
  input.value = 'test'
665
695
 
666
- const keyupEvent = new KeyboardEvent('keyup', { key: 'a', bubbles: true })
667
- Object.defineProperty(keyupEvent, 'target', { value: input })
668
- wrapper?.dispatchEvent(keyupEvent)
696
+ input.dispatchEvent(new Event('input', { bubbles: true }))
669
697
 
670
698
  await advanceTimers(300)
671
699
 
@@ -698,14 +726,11 @@ describe('Suggest', () => {
698
726
  await advanceTimers(50)
699
727
 
700
728
  const suggest = document.querySelector('shade-suggest') as HTMLElement
701
- const wrapper = suggest?.querySelector('.suggest-wrapper') as HTMLElement
702
729
 
703
730
  const input = suggest?.querySelector('input') as HTMLInputElement
704
731
  input.value = 'test'
705
732
 
706
- const keyupEvent = new KeyboardEvent('keyup', { key: 'a', bubbles: true })
707
- Object.defineProperty(keyupEvent, 'target', { value: input })
708
- wrapper?.dispatchEvent(keyupEvent)
733
+ input.dispatchEvent(new Event('input', { bubbles: true }))
709
734
 
710
735
  await advanceTimers(300)
711
736
 
@@ -742,13 +767,11 @@ describe('Suggest', () => {
742
767
  const input = suggest?.querySelector('input') as HTMLInputElement
743
768
  input.value = 'First'
744
769
 
745
- const keyupEvent = new KeyboardEvent('keyup', { key: 'a', bubbles: true })
746
- Object.defineProperty(keyupEvent, 'target', { value: input })
747
- wrapper?.dispatchEvent(keyupEvent)
770
+ input.dispatchEvent(new Event('input', { bubbles: true }))
748
771
 
749
772
  await advanceTimers(300)
750
773
 
751
- const enterEvent = new KeyboardEvent('keyup', { key: 'Enter', bubbles: true })
774
+ const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
752
775
  Object.defineProperty(enterEvent, 'target', { value: input })
753
776
  wrapper?.dispatchEvent(enterEvent)
754
777
 
@@ -784,15 +807,13 @@ describe('Suggest', () => {
784
807
  const input = suggest?.querySelector('input') as HTMLInputElement
785
808
  input.value = 'First'
786
809
 
787
- const keyupEvent = new KeyboardEvent('keyup', { key: 'a', bubbles: true })
788
- Object.defineProperty(keyupEvent, 'target', { value: input })
789
- wrapper?.dispatchEvent(keyupEvent)
810
+ input.dispatchEvent(new Event('input', { bubbles: true }))
790
811
 
791
812
  await advanceTimers(300)
792
813
 
793
814
  expect(suggest?.hasAttribute('data-opened')).toBe(true)
794
815
 
795
- const enterEvent = new KeyboardEvent('keyup', { key: 'Enter', bubbles: true })
816
+ const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
796
817
  Object.defineProperty(enterEvent, 'target', { value: input })
797
818
  wrapper?.dispatchEvent(enterEvent)
798
819
 
@@ -883,6 +904,89 @@ describe('Suggest', () => {
883
904
  })
884
905
  })
885
906
 
907
+ describe('spatial navigation attributes', () => {
908
+ it('should have data-spatial-nav-target on the host element', async () => {
909
+ await usingAsync(new Injector(), async (injector) => {
910
+ const rootElement = document.getElementById('root') as HTMLDivElement
911
+ const onSelectSuggestion = vi.fn()
912
+
913
+ initializeShadeRoot({
914
+ injector,
915
+ rootElement,
916
+ jsxElement: (
917
+ <Suggest<TestEntry>
918
+ defaultPrefix="🔍"
919
+ getEntries={getTestEntries}
920
+ getSuggestionEntry={getSuggestionEntry}
921
+ onSelectSuggestion={onSelectSuggestion}
922
+ />
923
+ ),
924
+ })
925
+
926
+ await advanceTimers(50)
927
+
928
+ const suggest = document.querySelector('shade-suggest') as HTMLElement
929
+ expect(suggest.hasAttribute('data-spatial-nav-target')).toBe(true)
930
+ })
931
+ })
932
+
933
+ it('should have tabIndex of -1 on the host element', async () => {
934
+ await usingAsync(new Injector(), async (injector) => {
935
+ const rootElement = document.getElementById('root') as HTMLDivElement
936
+ const onSelectSuggestion = vi.fn()
937
+
938
+ initializeShadeRoot({
939
+ injector,
940
+ rootElement,
941
+ jsxElement: (
942
+ <Suggest<TestEntry>
943
+ defaultPrefix="🔍"
944
+ getEntries={getTestEntries}
945
+ getSuggestionEntry={getSuggestionEntry}
946
+ onSelectSuggestion={onSelectSuggestion}
947
+ />
948
+ ),
949
+ })
950
+
951
+ await advanceTimers(50)
952
+
953
+ const suggest = document.querySelector('shade-suggest') as HTMLElement
954
+ expect(suggest.tabIndex).toBe(-1)
955
+ })
956
+ })
957
+
958
+ it('should delegate focus to the inner input when the host is focused', async () => {
959
+ await usingAsync(new Injector(), async (injector) => {
960
+ const rootElement = document.getElementById('root') as HTMLDivElement
961
+ const onSelectSuggestion = vi.fn()
962
+
963
+ initializeShadeRoot({
964
+ injector,
965
+ rootElement,
966
+ jsxElement: (
967
+ <Suggest<TestEntry>
968
+ defaultPrefix="🔍"
969
+ getEntries={getTestEntries}
970
+ getSuggestionEntry={getSuggestionEntry}
971
+ onSelectSuggestion={onSelectSuggestion}
972
+ />
973
+ ),
974
+ })
975
+
976
+ await advanceTimers(50)
977
+
978
+ const suggest = document.querySelector('shade-suggest') as HTMLElement
979
+ const input = suggest.querySelector('input') as HTMLInputElement
980
+
981
+ suggest.dispatchEvent(new FocusEvent('focus', { bubbles: false }))
982
+
983
+ await advanceTimers(10)
984
+
985
+ expect(document.activeElement).toBe(input)
986
+ })
987
+ })
988
+ })
989
+
886
990
  describe('synchronous suggestions mode', () => {
887
991
  it('should render with string[] suggestions', async () => {
888
992
  await usingAsync(new Injector(), async (injector) => {
@@ -956,7 +1060,7 @@ describe('Suggest', () => {
956
1060
  const input = suggest?.querySelector('input') as HTMLInputElement
957
1061
 
958
1062
  input.value = 'ap'
959
- input.dispatchEvent(new KeyboardEvent('keyup', { key: 'p', bubbles: true }))
1063
+ input.dispatchEvent(new Event('input', { bubbles: true }))
960
1064
 
961
1065
  await advanceTimers(300)
962
1066