@wordpress/components 37.0.0 → 38.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 (483) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +46 -0
  3. package/build/autocomplete/index.cjs +3 -1
  4. package/build/autocomplete/index.cjs.map +2 -2
  5. package/build/color-picker/component.cjs +14 -5
  6. package/build/color-picker/component.cjs.map +2 -2
  7. package/build/color-picker/picker.cjs +95 -23
  8. package/build/color-picker/picker.cjs.map +2 -2
  9. package/build/confirm-dialog/component.cjs +86 -4
  10. package/build/confirm-dialog/component.cjs.map +3 -3
  11. package/build/content-editable-control/index.cjs +19 -9
  12. package/build/content-editable-control/index.cjs.map +3 -3
  13. package/build/custom-select-control/index.cjs +1 -2
  14. package/build/custom-select-control/index.cjs.map +2 -2
  15. package/build/disabled/index.cjs +87 -4
  16. package/build/disabled/index.cjs.map +4 -4
  17. package/build/form-token-field/index.cjs +87 -2
  18. package/build/form-token-field/index.cjs.map +3 -3
  19. package/build/gradient-picker/index.cjs +26 -20
  20. package/build/gradient-picker/index.cjs.map +2 -2
  21. package/build/input-control/index.cjs +0 -1
  22. package/build/input-control/index.cjs.map +2 -2
  23. package/build/input-control/styles/input-control-styles.cjs +23 -31
  24. package/build/input-control/styles/input-control-styles.cjs.map +2 -2
  25. package/build/private-apis.cjs +0 -2
  26. package/build/private-apis.cjs.map +3 -3
  27. package/build/range-control/index.cjs +0 -1
  28. package/build/range-control/index.cjs.map +2 -2
  29. package/build/sandbox/index.cjs +23 -4
  30. package/build/sandbox/index.cjs.map +3 -3
  31. package/build/search-control/index.cjs +93 -10
  32. package/build/search-control/index.cjs.map +4 -4
  33. package/build/select-control/index.cjs +0 -1
  34. package/build/select-control/index.cjs.map +2 -2
  35. package/build/select-control/styles/select-control-styles.cjs +9 -16
  36. package/build/select-control/styles/select-control-styles.cjs.map +2 -2
  37. package/build/slot-fill/fill.cjs.map +2 -2
  38. package/build/toggle-group-control/toggle-group-control/component.cjs +91 -8
  39. package/build/toggle-group-control/toggle-group-control/component.cjs.map +4 -4
  40. package/build/toggle-group-control/toggle-group-control-option-base/component.cjs +98 -17
  41. package/build/toggle-group-control/toggle-group-control-option-base/component.cjs.map +4 -4
  42. package/build/tools-panel/tools-panel/component.cjs +6 -0
  43. package/build/tools-panel/tools-panel/component.cjs.map +2 -2
  44. package/build/tools-panel/tools-panel/hook.cjs +87 -11
  45. package/build/tools-panel/tools-panel/hook.cjs.map +4 -4
  46. package/build/tools-panel/tools-panel-header/component.cjs +87 -2
  47. package/build/tools-panel/tools-panel-header/component.cjs.map +3 -3
  48. package/build/tools-panel/tools-panel-header/hook.cjs +90 -20
  49. package/build/tools-panel/tools-panel-header/hook.cjs.map +4 -4
  50. package/build/tools-panel/tools-panel-item/hook.cjs +88 -9
  51. package/build/tools-panel/tools-panel-item/hook.cjs.map +4 -4
  52. package/build/validated-form-controls/control-with-error.cjs +3 -0
  53. package/build/validated-form-controls/control-with-error.cjs.map +2 -2
  54. package/build-module/autocomplete/index.mjs +3 -1
  55. package/build-module/autocomplete/index.mjs.map +2 -2
  56. package/build-module/color-picker/component.mjs +14 -5
  57. package/build-module/color-picker/component.mjs.map +2 -2
  58. package/build-module/color-picker/picker.mjs +96 -24
  59. package/build-module/color-picker/picker.mjs.map +2 -2
  60. package/build-module/confirm-dialog/component.mjs +86 -4
  61. package/build-module/confirm-dialog/component.mjs.map +3 -3
  62. package/build-module/content-editable-control/index.mjs +21 -11
  63. package/build-module/content-editable-control/index.mjs.map +2 -2
  64. package/build-module/custom-select-control/index.mjs +1 -2
  65. package/build-module/custom-select-control/index.mjs.map +2 -2
  66. package/build-module/disabled/index.mjs +87 -4
  67. package/build-module/disabled/index.mjs.map +3 -3
  68. package/build-module/form-token-field/index.mjs +88 -3
  69. package/build-module/form-token-field/index.mjs.map +3 -3
  70. package/build-module/gradient-picker/index.mjs +26 -20
  71. package/build-module/gradient-picker/index.mjs.map +2 -2
  72. package/build-module/input-control/index.mjs +0 -1
  73. package/build-module/input-control/index.mjs.map +2 -2
  74. package/build-module/input-control/styles/input-control-styles.mjs +23 -31
  75. package/build-module/input-control/styles/input-control-styles.mjs.map +2 -2
  76. package/build-module/private-apis.mjs +0 -2
  77. package/build-module/private-apis.mjs.map +2 -2
  78. package/build-module/range-control/index.mjs +0 -1
  79. package/build-module/range-control/index.mjs.map +2 -2
  80. package/build-module/sandbox/index.mjs +13 -4
  81. package/build-module/sandbox/index.mjs.map +2 -2
  82. package/build-module/search-control/index.mjs +93 -10
  83. package/build-module/search-control/index.mjs.map +3 -3
  84. package/build-module/select-control/index.mjs +0 -1
  85. package/build-module/select-control/index.mjs.map +2 -2
  86. package/build-module/select-control/styles/select-control-styles.mjs +9 -16
  87. package/build-module/select-control/styles/select-control-styles.mjs.map +2 -2
  88. package/build-module/slot-fill/fill.mjs.map +2 -2
  89. package/build-module/toggle-group-control/toggle-group-control/component.mjs +92 -9
  90. package/build-module/toggle-group-control/toggle-group-control/component.mjs.map +3 -3
  91. package/build-module/toggle-group-control/toggle-group-control-option-base/component.mjs +99 -18
  92. package/build-module/toggle-group-control/toggle-group-control-option-base/component.mjs.map +3 -3
  93. package/build-module/tools-panel/tools-panel/component.mjs +6 -0
  94. package/build-module/tools-panel/tools-panel/component.mjs.map +2 -2
  95. package/build-module/tools-panel/tools-panel/hook.mjs +87 -11
  96. package/build-module/tools-panel/tools-panel/hook.mjs.map +3 -3
  97. package/build-module/tools-panel/tools-panel-header/component.mjs +87 -2
  98. package/build-module/tools-panel/tools-panel-header/component.mjs.map +3 -3
  99. package/build-module/tools-panel/tools-panel-header/hook.mjs +90 -20
  100. package/build-module/tools-panel/tools-panel-header/hook.mjs.map +3 -3
  101. package/build-module/tools-panel/tools-panel-item/hook.mjs +89 -10
  102. package/build-module/tools-panel/tools-panel-item/hook.mjs.map +3 -3
  103. package/build-module/validated-form-controls/control-with-error.mjs +3 -0
  104. package/build-module/validated-form-controls/control-with-error.mjs.map +2 -2
  105. package/build-style/style-rtl.css +20 -24
  106. package/build-style/style.css +20 -24
  107. package/build-types/autocomplete/index.d.ts +7 -3
  108. package/build-types/autocomplete/index.d.ts.map +1 -1
  109. package/build-types/border-box-control/border-box-control/component.d.ts +1 -3
  110. package/build-types/border-box-control/border-box-control/component.d.ts.map +1 -1
  111. package/build-types/border-box-control/stories/index.story.d.ts +1 -3
  112. package/build-types/border-box-control/stories/index.story.d.ts.map +1 -1
  113. package/build-types/border-box-control/types.d.ts +1 -15
  114. package/build-types/border-box-control/types.d.ts.map +1 -1
  115. package/build-types/box-control/input-control.d.ts +3 -0
  116. package/build-types/box-control/input-control.d.ts.map +1 -0
  117. package/build-types/box-control/styles/box-control-styles.d.ts +4 -8
  118. package/build-types/box-control/styles/box-control-styles.d.ts.map +1 -1
  119. package/build-types/box-control/types.d.ts +1 -8
  120. package/build-types/box-control/types.d.ts.map +1 -1
  121. package/build-types/card/context.d.ts +3 -0
  122. package/build-types/card/context.d.ts.map +1 -0
  123. package/build-types/color-picker/component.d.ts.map +1 -1
  124. package/build-types/color-picker/picker.d.ts +17 -1
  125. package/build-types/color-picker/picker.d.ts.map +1 -1
  126. package/build-types/color-picker/styles.d.ts +4 -8
  127. package/build-types/color-picker/styles.d.ts.map +1 -1
  128. package/build-types/color-picker/types.d.ts +2 -0
  129. package/build-types/color-picker/types.d.ts.map +1 -1
  130. package/build-types/confirm-dialog/component.d.ts.map +1 -1
  131. package/build-types/content-editable-control/index.d.ts.map +1 -1
  132. package/build-types/context/wordpress-component.d.ts +2 -1
  133. package/build-types/context/wordpress-component.d.ts.map +1 -1
  134. package/build-types/custom-select-control/index.d.ts.map +1 -1
  135. package/build-types/custom-select-control/types.d.ts +1 -8
  136. package/build-types/custom-select-control/types.d.ts.map +1 -1
  137. package/build-types/date-time/date/index.d.ts +23 -0
  138. package/build-types/date-time/date/index.d.ts.map +1 -0
  139. package/build-types/date-time/date/styles.d.ts +45 -0
  140. package/build-types/date-time/date/styles.d.ts.map +1 -0
  141. package/build-types/date-time/date/test/index.d.ts +2 -0
  142. package/build-types/date-time/date/test/index.d.ts.map +1 -0
  143. package/build-types/date-time/date/test/use-lilius.d.ts +2 -0
  144. package/build-types/date-time/date/test/use-lilius.d.ts.map +1 -0
  145. package/build-types/date-time/date/use-lilius/index.d.ts +169 -0
  146. package/build-types/date-time/date/use-lilius/index.d.ts.map +1 -0
  147. package/build-types/date-time/time/index.d.ts +27 -0
  148. package/build-types/date-time/time/index.d.ts.map +1 -0
  149. package/build-types/date-time/time/styles.d.ts +93 -0
  150. package/build-types/date-time/time/styles.d.ts.map +1 -0
  151. package/build-types/date-time/time/test/index.d.ts +2 -0
  152. package/build-types/date-time/time/test/index.d.ts.map +1 -0
  153. package/build-types/date-time/time/time-input/index.d.ts +4 -0
  154. package/build-types/date-time/time/time-input/index.d.ts.map +1 -0
  155. package/build-types/date-time/time/time-input/test/index.d.ts +2 -0
  156. package/build-types/date-time/time/time-input/test/index.d.ts.map +1 -0
  157. package/build-types/date-time/time/timezone.d.ts +7 -0
  158. package/build-types/date-time/time/timezone.d.ts.map +1 -0
  159. package/build-types/date-time/time-picker/styles.d.ts +8 -16
  160. package/build-types/date-time/time-picker/styles.d.ts.map +1 -1
  161. package/build-types/disabled/index.d.ts.map +1 -1
  162. package/build-types/divider/styles.d.ts +10 -0
  163. package/build-types/divider/styles.d.ts.map +1 -0
  164. package/build-types/flex/styles.d.ts +17 -0
  165. package/build-types/flex/styles.d.ts.map +1 -0
  166. package/build-types/focal-point-picker/styles/focal-point-picker-style.d.ts +2 -4
  167. package/build-types/focal-point-picker/styles/focal-point-picker-style.d.ts.map +1 -1
  168. package/build-types/font-size-picker/index.d.ts +12 -2
  169. package/build-types/font-size-picker/index.d.ts.map +1 -1
  170. package/build-types/font-size-picker/stories/e2e/index.story.d.ts +12 -1
  171. package/build-types/font-size-picker/stories/e2e/index.story.d.ts.map +1 -1
  172. package/build-types/font-size-picker/types.d.ts +2 -15
  173. package/build-types/font-size-picker/types.d.ts.map +1 -1
  174. package/build-types/form-token-field/index.d.ts.map +1 -1
  175. package/build-types/gradient-picker/index.d.ts.map +1 -1
  176. package/build-types/gradient-picker/stories/index.story.d.ts +1 -0
  177. package/build-types/gradient-picker/stories/index.story.d.ts.map +1 -1
  178. package/build-types/gradient-picker/test/index.d.ts +2 -0
  179. package/build-types/gradient-picker/test/index.d.ts.map +1 -0
  180. package/build-types/gradient-picker/types.d.ts +19 -4
  181. package/build-types/gradient-picker/types.d.ts.map +1 -1
  182. package/build-types/input-control/index.d.ts.map +1 -1
  183. package/build-types/input-control/styles/input-control-styles.d.ts +0 -6
  184. package/build-types/input-control/styles/input-control-styles.d.ts.map +1 -1
  185. package/build-types/input-control/types.d.ts +1 -8
  186. package/build-types/input-control/types.d.ts.map +1 -1
  187. package/build-types/mobile/image/constants.d.ts +5 -0
  188. package/build-types/mobile/image/constants.d.ts.map +1 -0
  189. package/build-types/navigation/back-button/index.d.ts +7 -0
  190. package/build-types/navigation/back-button/index.d.ts.map +1 -0
  191. package/build-types/navigation/constants.d.ts +3 -0
  192. package/build-types/navigation/constants.d.ts.map +1 -0
  193. package/build-types/navigation/context.d.ts +4 -0
  194. package/build-types/navigation/context.d.ts.map +1 -0
  195. package/build-types/navigation/group/context.d.ts +7 -0
  196. package/build-types/navigation/group/context.d.ts.map +1 -0
  197. package/build-types/navigation/group/index.d.ts +7 -0
  198. package/build-types/navigation/group/index.d.ts.map +1 -0
  199. package/build-types/navigation/index.d.ts +46 -0
  200. package/build-types/navigation/index.d.ts.map +1 -0
  201. package/build-types/navigation/item/base-content.d.ts +3 -0
  202. package/build-types/navigation/item/base-content.d.ts.map +1 -0
  203. package/build-types/navigation/item/base.d.ts +3 -0
  204. package/build-types/navigation/item/base.d.ts.map +1 -0
  205. package/build-types/navigation/item/index.d.ts +7 -0
  206. package/build-types/navigation/item/index.d.ts.map +1 -0
  207. package/build-types/navigation/item/use-navigation-tree-item.d.ts +3 -0
  208. package/build-types/navigation/item/use-navigation-tree-item.d.ts.map +1 -0
  209. package/build-types/navigation/menu/context.d.ts +7 -0
  210. package/build-types/navigation/menu/context.d.ts.map +1 -0
  211. package/build-types/navigation/menu/index.d.ts +7 -0
  212. package/build-types/navigation/menu/index.d.ts.map +1 -0
  213. package/build-types/navigation/menu/menu-title-search.d.ts +3 -0
  214. package/build-types/navigation/menu/menu-title-search.d.ts.map +1 -0
  215. package/build-types/navigation/menu/menu-title.d.ts +3 -0
  216. package/build-types/navigation/menu/menu-title.d.ts.map +1 -0
  217. package/build-types/navigation/menu/search-no-results-found.d.ts +3 -0
  218. package/build-types/navigation/menu/search-no-results-found.d.ts.map +1 -0
  219. package/build-types/navigation/menu/use-navigation-tree-menu.d.ts +3 -0
  220. package/build-types/navigation/menu/use-navigation-tree-menu.d.ts.map +1 -0
  221. package/build-types/navigation/stories/index.story.d.ts +23 -0
  222. package/build-types/navigation/stories/index.story.d.ts.map +1 -0
  223. package/build-types/navigation/stories/utils/controlled-state.d.ts +7 -0
  224. package/build-types/navigation/stories/utils/controlled-state.d.ts.map +1 -0
  225. package/build-types/navigation/stories/utils/default.d.ts +10 -0
  226. package/build-types/navigation/stories/utils/default.d.ts.map +1 -0
  227. package/build-types/navigation/stories/utils/group.d.ts +10 -0
  228. package/build-types/navigation/stories/utils/group.d.ts.map +1 -0
  229. package/build-types/navigation/stories/utils/hide-if-empty.d.ts +10 -0
  230. package/build-types/navigation/stories/utils/hide-if-empty.d.ts.map +1 -0
  231. package/build-types/navigation/stories/utils/more-examples.d.ts +10 -0
  232. package/build-types/navigation/stories/utils/more-examples.d.ts.map +1 -0
  233. package/build-types/navigation/stories/utils/search.d.ts +10 -0
  234. package/build-types/navigation/stories/utils/search.d.ts.map +1 -0
  235. package/build-types/navigation/styles/navigation-styles.d.ts +55 -0
  236. package/build-types/navigation/styles/navigation-styles.d.ts.map +1 -0
  237. package/build-types/navigation/test/index.d.ts +2 -0
  238. package/build-types/navigation/test/index.d.ts.map +1 -0
  239. package/build-types/navigation/types.d.ts +266 -0
  240. package/build-types/navigation/types.d.ts.map +1 -0
  241. package/build-types/navigation/use-create-navigation-tree.d.ts +15 -0
  242. package/build-types/navigation/use-create-navigation-tree.d.ts.map +1 -0
  243. package/build-types/navigation/use-navigation-tree-nodes.d.ts +10 -0
  244. package/build-types/navigation/use-navigation-tree-nodes.d.ts.map +1 -0
  245. package/build-types/navigation/utils.d.ts +3 -0
  246. package/build-types/navigation/utils.d.ts.map +1 -0
  247. package/build-types/number-control/index.d.ts +2 -4
  248. package/build-types/number-control/index.d.ts.map +1 -1
  249. package/build-types/number-control/stories/index.story.d.ts +2 -4
  250. package/build-types/number-control/stories/index.story.d.ts.map +1 -1
  251. package/build-types/number-control/types.d.ts +1 -15
  252. package/build-types/number-control/types.d.ts.map +1 -1
  253. package/build-types/private-apis.d.ts.map +1 -1
  254. package/build-types/range-control/index.d.ts +2 -4
  255. package/build-types/range-control/index.d.ts.map +1 -1
  256. package/build-types/range-control/styles/range-control-styles.d.ts +2 -4
  257. package/build-types/range-control/styles/range-control-styles.d.ts.map +1 -1
  258. package/build-types/range-control/types.d.ts +2 -16
  259. package/build-types/range-control/types.d.ts.map +1 -1
  260. package/build-types/sandbox/index.d.ts.map +1 -1
  261. package/build-types/sandbox/types.d.ts +11 -0
  262. package/build-types/sandbox/types.d.ts.map +1 -1
  263. package/build-types/search-control/index.d.ts.map +1 -1
  264. package/build-types/search-control/types.d.ts +1 -1
  265. package/build-types/search-control/types.d.ts.map +1 -1
  266. package/build-types/select-control/index.d.ts.map +1 -1
  267. package/build-types/select-control/styles/select-control-styles.d.ts.map +1 -1
  268. package/build-types/select-control/types.d.ts +0 -7
  269. package/build-types/select-control/types.d.ts.map +1 -1
  270. package/build-types/slot-fill/fill.d.ts.map +1 -1
  271. package/build-types/slot-fill/stories/index.story.d.ts +1 -6
  272. package/build-types/slot-fill/stories/index.story.d.ts.map +1 -1
  273. package/build-types/surface/styles.d.ts +11 -0
  274. package/build-types/surface/styles.d.ts.map +1 -0
  275. package/build-types/theme/styles.d.ts +10 -0
  276. package/build-types/theme/styles.d.ts.map +1 -0
  277. package/build-types/toggle-group-control/toggle-group-control/component.d.ts +0 -1
  278. package/build-types/toggle-group-control/toggle-group-control/component.d.ts.map +1 -1
  279. package/build-types/toggle-group-control/toggle-group-control-option-base/component.d.ts.map +1 -1
  280. package/build-types/toggle-group-control/types.d.ts +0 -7
  281. package/build-types/toggle-group-control/types.d.ts.map +1 -1
  282. package/build-types/tools-panel/tools-panel/component.d.ts.map +1 -1
  283. package/build-types/tools-panel/tools-panel/hook.d.ts.map +1 -1
  284. package/build-types/tools-panel/tools-panel-header/component.d.ts.map +1 -1
  285. package/build-types/tools-panel/tools-panel-header/hook.d.ts.map +1 -1
  286. package/build-types/tools-panel/tools-panel-item/hook.d.ts.map +1 -1
  287. package/build-types/tree-select/types.d.ts +1 -8
  288. package/build-types/tree-select/types.d.ts.map +1 -1
  289. package/build-types/truncate/styles.d.ts +2 -0
  290. package/build-types/truncate/styles.d.ts.map +1 -0
  291. package/build-types/unit-control/index.d.ts +2 -4
  292. package/build-types/unit-control/index.d.ts.map +1 -1
  293. package/build-types/unit-control/styles/unit-control-styles.d.ts +2 -4
  294. package/build-types/unit-control/styles/unit-control-styles.d.ts.map +1 -1
  295. package/build-types/unit-control/types.d.ts +1 -15
  296. package/build-types/unit-control/types.d.ts.map +1 -1
  297. package/build-types/utils/deprecated-36px-size.d.ts +7 -0
  298. package/build-types/utils/deprecated-36px-size.d.ts.map +1 -0
  299. package/build-types/validated-form-controls/components/number-control.d.ts +2 -4
  300. package/build-types/validated-form-controls/components/number-control.d.ts.map +1 -1
  301. package/build-types/validated-form-controls/components/range-control.d.ts +2 -4
  302. package/build-types/validated-form-controls/components/range-control.d.ts.map +1 -1
  303. package/build-types/validated-form-controls/components/stories/overview.story.d.ts +5 -0
  304. package/build-types/validated-form-controls/components/stories/overview.story.d.ts.map +1 -1
  305. package/build-types/validated-form-controls/components/toggle-group-control.d.ts +1 -2
  306. package/build-types/validated-form-controls/components/toggle-group-control.d.ts.map +1 -1
  307. package/build-types/validated-form-controls/control-with-error.d.ts.map +1 -1
  308. package/build-types/visually-hidden/test/index.d.ts +2 -0
  309. package/build-types/visually-hidden/test/index.d.ts.map +1 -0
  310. package/package.json +27 -27
  311. package/src/autocomplete/index.tsx +16 -3
  312. package/src/base-control/README.md +2 -2
  313. package/src/border-box-control/types.ts +4 -15
  314. package/src/box-control/types.ts +4 -8
  315. package/src/card/test/__snapshots__/index.tsx.snap +1 -1
  316. package/src/checkbox-control/test/__snapshots__/index.tsx.snap +1 -1
  317. package/src/color-indicator/test/__snapshots__/index.tsx.snap +1 -1
  318. package/src/color-picker/component.tsx +23 -19
  319. package/src/color-picker/picker.tsx +144 -21
  320. package/src/color-picker/test/index.tsx +502 -0
  321. package/src/color-picker/types.ts +2 -0
  322. package/src/combobox-control/style.scss +2 -2
  323. package/src/confirm-dialog/component.tsx +2 -4
  324. package/src/confirm-dialog/style.module.scss +7 -0
  325. package/src/confirm-dialog/test/index.tsx +5 -0
  326. package/src/content-editable-control/index.tsx +24 -5
  327. package/src/content-editable-control/style.module.scss +1 -1
  328. package/src/content-editable-control/test/index.tsx +20 -8
  329. package/src/context/test/__snapshots__/context-system-provider.js.snap +1 -1
  330. package/src/context/wordpress-component.ts +7 -2
  331. package/src/custom-gradient-picker/test/index.tsx +111 -1
  332. package/src/custom-select-control/index.tsx +1 -2
  333. package/src/custom-select-control/test/__snapshots__/index.tsx.snap +6 -0
  334. package/src/custom-select-control/test/index.tsx +23 -0
  335. package/src/custom-select-control/types.ts +1 -8
  336. package/src/disabled/index.tsx +7 -5
  337. package/src/disabled/style.module.scss +15 -0
  338. package/src/disabled/test/index.tsx +6 -4
  339. package/src/elevation/test/__snapshots__/index.tsx.snap +1 -1
  340. package/src/external-link/style.scss +6 -12
  341. package/src/flex/test/__snapshots__/index.tsx.snap +1 -1
  342. package/src/font-size-picker/types.ts +6 -15
  343. package/src/form-toggle/test/__snapshots__/index.tsx.snap +1 -1
  344. package/src/form-token-field/index.tsx +5 -4
  345. package/src/form-token-field/style.module.scss +6 -0
  346. package/src/form-token-field/style.scss +2 -2
  347. package/src/gradient-picker/README.md +21 -3
  348. package/src/gradient-picker/index.tsx +43 -28
  349. package/src/gradient-picker/stories/index.story.tsx +41 -5
  350. package/src/gradient-picker/test/index.tsx +148 -0
  351. package/src/gradient-picker/types.ts +24 -4
  352. package/src/h-stack/test/__snapshots__/index.tsx.snap +1 -1
  353. package/src/heading/test/__snapshots__/index.tsx.snap +1 -1
  354. package/src/higher-order/with-filters/test/__snapshots__/index.tsx.snap +1 -1
  355. package/src/input-control/index.tsx +0 -1
  356. package/src/input-control/styles/input-control-styles.tsx +0 -8
  357. package/src/input-control/test/__snapshots__/index.js.snap +11 -0
  358. package/src/input-control/test/index.js +28 -0
  359. package/src/input-control/types.ts +1 -8
  360. package/src/item-group/test/__snapshots__/index.js.snap +1 -1
  361. package/src/menu/README.md +2 -2
  362. package/src/menu-item/test/__snapshots__/index.js.snap +1 -1
  363. package/src/modal/test/index.tsx +42 -0
  364. package/src/notice/test/__snapshots__/index.tsx.snap +1 -1
  365. package/src/number-control/types.ts +1 -22
  366. package/src/panel/test/__snapshots__/body.tsx.snap +1 -1
  367. package/src/panel/test/__snapshots__/header.tsx.snap +1 -1
  368. package/src/panel/test/__snapshots__/index.tsx.snap +1 -1
  369. package/src/panel/test/__snapshots__/row.tsx.snap +1 -1
  370. package/src/private-apis.ts +0 -2
  371. package/src/range-control/index.tsx +0 -1
  372. package/src/range-control/types.ts +2 -15
  373. package/src/sandbox/index.tsx +22 -2
  374. package/src/sandbox/test/index.tsx +21 -0
  375. package/src/sandbox/types.ts +11 -0
  376. package/src/scrollable/test/__snapshots__/index.tsx.snap +1 -1
  377. package/src/search-control/index.tsx +23 -20
  378. package/src/search-control/style.module.scss +16 -0
  379. package/src/search-control/test/index.tsx +8 -0
  380. package/src/search-control/types.ts +1 -1
  381. package/src/select-control/index.tsx +0 -1
  382. package/src/select-control/styles/select-control-styles.ts +0 -7
  383. package/src/select-control/test/__snapshots__/select-control.tsx.snap +11 -0
  384. package/src/select-control/test/select-control.tsx +37 -0
  385. package/src/select-control/types.ts +0 -7
  386. package/src/shortcut/test/__snapshots__/index.tsx.snap +1 -1
  387. package/src/slot-fill/fill.tsx +3 -9
  388. package/src/slot-fill/stories/index.story.tsx +52 -10
  389. package/src/slot-fill/test/__snapshots__/slot.js.snap +1 -1
  390. package/src/slot-fill/test/slot.js +53 -11
  391. package/src/spacer/test/__snapshots__/index.tsx.snap +1 -1
  392. package/src/text/test/__snapshots__/index.tsx.snap +1 -1
  393. package/src/text-control/style.scss +1 -1
  394. package/src/toggle-group-control/test/__snapshots__/index.tsx.snap +29 -707
  395. package/src/toggle-group-control/test/index.tsx +33 -3
  396. package/src/toggle-group-control/toggle-group-control/component.tsx +11 -16
  397. package/src/toggle-group-control/toggle-group-control/style.module.scss +63 -0
  398. package/src/toggle-group-control/toggle-group-control-option-base/component.tsx +22 -24
  399. package/src/toggle-group-control/toggle-group-control-option-base/style.module.scss +88 -0
  400. package/src/toggle-group-control/types.ts +0 -7
  401. package/src/tools-panel/style.module.scss +117 -0
  402. package/src/tools-panel/test/index.tsx +105 -39
  403. package/src/tools-panel/tools-panel/component.tsx +12 -1
  404. package/src/tools-panel/tools-panel/hook.ts +13 -15
  405. package/src/tools-panel/tools-panel-header/component.tsx +6 -2
  406. package/src/tools-panel/tools-panel-header/hook.ts +7 -25
  407. package/src/tools-panel/tools-panel-item/hook.ts +16 -34
  408. package/src/tooltip/test/index.tsx +1 -1
  409. package/src/tree-grid/test/__snapshots__/cell.tsx.snap +1 -1
  410. package/src/tree-grid/test/__snapshots__/index.tsx.snap +1 -1
  411. package/src/tree-grid/test/__snapshots__/roving-tab-index-item.tsx.snap +1 -1
  412. package/src/tree-grid/test/__snapshots__/roving-tab-index.tsx.snap +1 -1
  413. package/src/tree-grid/test/__snapshots__/row.tsx.snap +1 -1
  414. package/src/tree-select/README.md +1 -1
  415. package/src/tree-select/types.ts +1 -8
  416. package/src/unit-control/types.ts +1 -18
  417. package/src/utils/theme-variables.scss +1 -0
  418. package/src/v-stack/test/__snapshots__/index.tsx.snap +1 -1
  419. package/src/validated-form-controls/components/stories/overview.story.tsx +51 -0
  420. package/src/validated-form-controls/control-with-error.tsx +9 -0
  421. package/src/validated-form-controls/style.scss +3 -3
  422. package/src/validated-form-controls/test/control-with-error.tsx +42 -0
  423. package/src/view/test/__snapshots__/index.js.snap +1 -1
  424. package/build/confirm-dialog/styles.cjs +0 -42
  425. package/build/confirm-dialog/styles.cjs.map +0 -7
  426. package/build/disabled/styles/disabled-styles.cjs +0 -42
  427. package/build/disabled/styles/disabled-styles.cjs.map +0 -7
  428. package/build/form-token-field/styles.cjs +0 -49
  429. package/build/form-token-field/styles.cjs.map +0 -7
  430. package/build/search-control/styles.cjs +0 -74
  431. package/build/search-control/styles.cjs.map +0 -7
  432. package/build/theme/color-algorithms.cjs +0 -131
  433. package/build/theme/color-algorithms.cjs.map +0 -7
  434. package/build/theme/index.cjs +0 -160
  435. package/build/theme/index.cjs.map +0 -7
  436. package/build/theme/types.cjs +0 -19
  437. package/build/theme/types.cjs.map +0 -7
  438. package/build/toggle-group-control/toggle-group-control/styles.cjs +0 -53
  439. package/build/toggle-group-control/toggle-group-control/styles.cjs.map +0 -7
  440. package/build/toggle-group-control/toggle-group-control-option-base/styles.cjs +0 -94
  441. package/build/toggle-group-control/toggle-group-control-option-base/styles.cjs.map +0 -7
  442. package/build/tools-panel/styles.cjs +0 -129
  443. package/build/tools-panel/styles.cjs.map +0 -7
  444. package/build/utils/use-deprecated-props.cjs +0 -41
  445. package/build/utils/use-deprecated-props.cjs.map +0 -7
  446. package/build-module/confirm-dialog/styles.mjs +0 -17
  447. package/build-module/confirm-dialog/styles.mjs.map +0 -7
  448. package/build-module/disabled/styles/disabled-styles.mjs +0 -17
  449. package/build-module/disabled/styles/disabled-styles.mjs.map +0 -7
  450. package/build-module/form-token-field/styles.mjs +0 -14
  451. package/build-module/form-token-field/styles.mjs.map +0 -7
  452. package/build-module/search-control/styles.mjs +0 -38
  453. package/build-module/search-control/styles.mjs.map +0 -7
  454. package/build-module/theme/color-algorithms.mjs +0 -94
  455. package/build-module/theme/color-algorithms.mjs.map +0 -7
  456. package/build-module/theme/index.mjs +0 -129
  457. package/build-module/theme/index.mjs.map +0 -7
  458. package/build-module/theme/types.mjs +0 -1
  459. package/build-module/theme/types.mjs.map +0 -7
  460. package/build-module/toggle-group-control/toggle-group-control/styles.mjs +0 -27
  461. package/build-module/toggle-group-control/toggle-group-control/styles.mjs.map +0 -7
  462. package/build-module/toggle-group-control/toggle-group-control-option-base/styles.mjs +0 -56
  463. package/build-module/toggle-group-control/toggle-group-control-option-base/styles.mjs.map +0 -7
  464. package/build-module/tools-panel/styles.mjs +0 -84
  465. package/build-module/tools-panel/styles.mjs.map +0 -7
  466. package/build-module/utils/use-deprecated-props.mjs +0 -16
  467. package/build-module/utils/use-deprecated-props.mjs.map +0 -7
  468. package/src/confirm-dialog/styles.ts +0 -18
  469. package/src/disabled/styles/disabled-styles.tsx +0 -23
  470. package/src/form-token-field/styles.ts +0 -15
  471. package/src/search-control/styles.ts +0 -27
  472. package/src/theme/README.md +0 -47
  473. package/src/theme/color-algorithms.ts +0 -142
  474. package/src/theme/index.tsx +0 -80
  475. package/src/theme/stories/index.story.tsx +0 -127
  476. package/src/theme/style.module.scss +0 -4
  477. package/src/theme/test/color-algorithms.ts +0 -100
  478. package/src/theme/test/index.tsx +0 -151
  479. package/src/theme/types.ts +0 -61
  480. package/src/toggle-group-control/toggle-group-control/styles.ts +0 -84
  481. package/src/toggle-group-control/toggle-group-control-option-base/styles.ts +0 -109
  482. package/src/tools-panel/styles.ts +0 -176
  483. package/src/utils/use-deprecated-props.ts +0 -14
@@ -1,42 +1,165 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { HslColorPicker, HslaColorPicker } from 'react-colorful';
4
+ import type { PointerEvent } from 'react';
5
+ import { useEffect, useRef, useState } from '@wordpress/element';
6
+ import { HsvColorPicker, HsvaColorPicker } from 'react-colorful';
7
+ import { colord } from 'colord';
8
+ import type { HslaColor, HsvaColor } from 'react-colorful';
5
9
 
6
10
  /**
7
11
  * Internal dependencies
8
12
  */
9
13
  import type { PickerProps } from './types';
10
14
 
11
- // Pointer capture fortifies drag gestures so that they continue to work
12
- // while dragging outside the component over objects like iframes. If a
13
- // newer version of react-colorful begins to employ pointer capture this
14
- // will be redundant and should be removed.
15
- const pointerCaptureProps = {
16
- onPointerDown( { currentTarget, pointerId }: React.PointerEvent ) {
17
- currentTarget.setPointerCapture( pointerId );
18
- },
19
- onPointerUp( { currentTarget, pointerId }: React.PointerEvent ) {
20
- currentTarget.releasePointerCapture( pointerId );
21
- },
22
- };
15
+ function toHsva( hsla: PickerProps[ 'hsla' ] ): HsvaColor {
16
+ return {
17
+ ...colord( hsla ).toHsv(),
18
+ // HSL and HSV share the hue angle. Color conversion collapses achromatic
19
+ // hue to 0, but HSLA retains the user's latent hue.
20
+ h: hsla.h,
21
+ a: hsla.a,
22
+ };
23
+ }
24
+
25
+ function isSameHsla( a: HslaColor, b: HslaColor ): boolean {
26
+ return a.h === b.h && a.s === b.s && a.l === b.l && a.a === b.a;
27
+ }
28
+
29
+ /**
30
+ * Convert parent HSLA into HSVA for prop sync.
31
+ *
32
+ * At black, RGB round-trips collapse saturation to 0. Preserve the native
33
+ * HSVA saturation coordinate unless the HSL saturation channel itself
34
+ * changed (sibling hue/alpha edits must not snap the pointer).
35
+ *
36
+ * At white, only `v: 100, s: 0` is visually white — preserving a prior
37
+ * chromatic saturation would leave the pointer at the top-right and make
38
+ * the first keyboard step jump from white to a saturated color.
39
+ */
40
+ function toHsvaFromHsla(
41
+ hsla: HslaColor,
42
+ prevHsva: HsvaColor,
43
+ prevHsla: HslaColor | null
44
+ ): HsvaColor {
45
+ const converted = toHsva( hsla );
46
+
47
+ // White and chromatic colors use the converted value as-is.
48
+ if ( hsla.l !== 0 ) {
49
+ return converted;
50
+ }
51
+
52
+ const saturationChanged = prevHsla !== null && prevHsla.s !== hsla.s;
53
+
54
+ return {
55
+ h: hsla.h,
56
+ s: saturationChanged ? hsla.s : prevHsva.s,
57
+ v: 0,
58
+ a: hsla.a,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Visual color surface.
64
+ *
65
+ * Uses HSVA (react-colorful's native model) and keeps that value in local
66
+ * state so HSLA↔hex round-trips cannot move the pointer
67
+ * Parent ColorPicker still speaks HSLA for
68
+ * inputs and controlled value sync; conversion happens only at the boundary.
69
+ *
70
+ * Prop sync from parent HSLA is suppressed for:
71
+ * - pointer/touch drags (interaction flag), and
72
+ * - any picker-originated update including keyboard (HSLA origin token).
73
+ *
74
+ * Achromatic HSL sibling edits (hue/alpha while at black) preserve native
75
+ * HSVA saturation unless saturation itself changed. White always syncs to
76
+ * the converted `s: 0` visual coordinate.
77
+ */
78
+ export const Picker = ( {
79
+ hsla,
80
+ enableAlpha,
81
+ onChange,
82
+ onInteractionStart,
83
+ onInteractionEnd,
84
+ }: PickerProps ) => {
85
+ const [ hsva, setHsva ] = useState< HsvaColor >( () => toHsva( hsla ) );
86
+ // Last HSLA emitted by this picker — skip echoing it back into HSVA.
87
+ const pickerOriginHslaRef = useRef< HslaColor | null >( null );
88
+ // Previous parent HSLA — detect which channel changed on achromatic sync.
89
+ const prevHslaRef = useRef< HslaColor >( hsla );
90
+ // Pointer/touch drag: never sync from HSLA mid-gesture (origin match alone
91
+ // can fail across rapid frames when parent gradient state re-renders).
92
+ const isPointerInteractingRef = useRef( false );
93
+
94
+ useEffect( () => {
95
+ if ( isPointerInteractingRef.current ) {
96
+ // Keep prevHsla in lockstep with parent updates during the gesture
97
+ // so a post-drag HSL sibling edit is not compared against pre-drag
98
+ // HSLA and mistaken for a saturation change.
99
+ prevHslaRef.current = hsla;
100
+ return;
101
+ }
102
+ if (
103
+ pickerOriginHslaRef.current &&
104
+ isSameHsla( pickerOriginHslaRef.current, hsla )
105
+ ) {
106
+ prevHslaRef.current = hsla;
107
+ return;
108
+ }
109
+ pickerOriginHslaRef.current = null;
110
+ setHsva( ( prev ) =>
111
+ toHsvaFromHsla( hsla, prev, prevHslaRef.current )
112
+ );
113
+ prevHslaRef.current = hsla;
114
+ }, [ hsla ] );
115
+
116
+ // Inline handlers so refs are not passed into a helper during render
117
+ // (react-hooks/refs). Pointer capture also keeps drag working over iframes.
118
+ const pointerCaptureProps = {
119
+ onPointerDown( { currentTarget, pointerId }: PointerEvent ) {
120
+ isPointerInteractingRef.current = true;
121
+ onInteractionStart?.();
122
+ currentTarget.setPointerCapture( pointerId );
123
+ },
124
+ onPointerUp( { currentTarget, pointerId }: PointerEvent ) {
125
+ currentTarget.releasePointerCapture( pointerId );
126
+ isPointerInteractingRef.current = false;
127
+ onInteractionEnd?.();
128
+ },
129
+ onPointerCancel( { currentTarget, pointerId }: PointerEvent ) {
130
+ currentTarget.releasePointerCapture( pointerId );
131
+ isPointerInteractingRef.current = false;
132
+ onInteractionEnd?.();
133
+ },
134
+ };
135
+
136
+ const handleChange = ( next: HsvaColor ) => {
137
+ setHsva( next );
138
+ const nextHsla: HslaColor = { ...colord( next ).toHsl(), a: next.a };
139
+ pickerOriginHslaRef.current = nextHsla;
140
+ onChange( nextHsla );
141
+ };
23
142
 
24
- export const Picker = ( { hsla, enableAlpha, onChange }: PickerProps ) => {
25
143
  if ( enableAlpha ) {
26
144
  return (
27
- <HslaColorPicker
28
- color={ hsla }
29
- onChange={ onChange }
145
+ <HsvaColorPicker
146
+ color={ hsva }
147
+ onChange={ handleChange }
30
148
  { ...pointerCaptureProps }
31
149
  />
32
150
  );
33
151
  }
34
152
 
153
+ // HsvColorPicker's equality checks enumerate own keys — never pass `a`,
154
+ // or every parent re-render looks like an external color change and the
155
+ // pointer jitters while dragging.
156
+ const hsv = { h: hsva.h, s: hsva.s, v: hsva.v };
157
+
35
158
  return (
36
- <HslColorPicker
37
- color={ hsla }
38
- onChange={ ( nextColor ) => {
39
- onChange( { ...nextColor, a: hsla.a } );
159
+ <HsvColorPicker
160
+ color={ hsv }
161
+ onChange={ ( next ) => {
162
+ handleChange( { ...next, a: hsva.a } );
40
163
  } }
41
164
  { ...pointerCaptureProps }
42
165
  />
@@ -378,6 +378,102 @@ describe( 'ColorPicker', () => {
378
378
  expect( onChange ).toHaveBeenLastCalledWith( '#000000' );
379
379
  } );
380
380
 
381
+ it.each( [
382
+ [ 'black', 0 ],
383
+ [ 'white', 100 ],
384
+ ] )(
385
+ 'should allow saturation 50→0 at %s without firing onChange',
386
+ async ( _label, lightness ) => {
387
+ const onChange = jest.fn();
388
+
389
+ render(
390
+ <ControlledColorPicker
391
+ onChange={ onChange }
392
+ enableAlpha={ false }
393
+ initialColor="#000000"
394
+ />
395
+ );
396
+
397
+ const formatSelector = screen.getByRole( 'combobox' );
398
+ await userEvent.setup().selectOptions( formatSelector, 'hsl' );
399
+
400
+ const hueSlider = screen
401
+ .getAllByRole( 'slider', { name: 'Hue' } )
402
+ .at( -1 )!;
403
+ const saturationSlider = screen.getByRole( 'slider', {
404
+ name: 'Saturation',
405
+ } );
406
+ const lightnessSlider = screen.getByRole( 'slider', {
407
+ name: 'Lightness',
408
+ } );
409
+ const saturationNumberInput = screen.getByRole( 'spinbutton', {
410
+ name: 'Saturation',
411
+ } );
412
+
413
+ // Build hsl(200, 50%, lightness%) via HSL controls.
414
+ fireEvent.change( hueSlider, { target: { value: 200 } } );
415
+ fireEvent.change( saturationSlider, { target: { value: 50 } } );
416
+ fireEvent.change( lightnessSlider, {
417
+ target: { value: lightness },
418
+ } );
419
+ await waitFor( () =>
420
+ expect( lightnessSlider ).toHaveValue( String( lightness ) )
421
+ );
422
+
423
+ expect( saturationSlider ).toHaveValue( '50' );
424
+ expect( saturationNumberInput ).toHaveValue( 50 );
425
+ onChange.mockClear();
426
+
427
+ fireEvent.change( saturationSlider, { target: { value: 0 } } );
428
+
429
+ expect( saturationSlider ).toHaveValue( '0' );
430
+ expect( saturationNumberInput ).toHaveValue( 0 );
431
+ expect( onChange ).not.toHaveBeenCalled();
432
+ }
433
+ );
434
+
435
+ it( 'should fire onChange once per real color change in controlled mode', async () => {
436
+ const onChange = jest.fn();
437
+
438
+ render(
439
+ <ControlledColorPicker
440
+ onChange={ onChange }
441
+ enableAlpha={ false }
442
+ initialColor="#2ad5d5" // hsl(180, 67%, 50%)
443
+ />
444
+ );
445
+
446
+ const formatSelector = screen.getByRole( 'combobox' );
447
+ await userEvent.setup().selectOptions( formatSelector, 'hsl' );
448
+
449
+ const lightnessSlider = screen.getByRole( 'slider', {
450
+ name: 'Lightness',
451
+ } );
452
+
453
+ fireEvent.change( lightnessSlider, { target: { value: 40 } } );
454
+ await waitFor( () =>
455
+ expect( lightnessSlider ).toHaveValue( '40' )
456
+ );
457
+ expect( onChange ).toHaveBeenCalledTimes( 1 );
458
+
459
+ fireEvent.change( lightnessSlider, { target: { value: 30 } } );
460
+ await waitFor( () =>
461
+ expect( lightnessSlider ).toHaveValue( '30' )
462
+ );
463
+ expect( onChange ).toHaveBeenCalledTimes( 2 );
464
+
465
+ fireEvent.change( lightnessSlider, { target: { value: 0 } } );
466
+ await waitFor( () => expect( lightnessSlider ).toHaveValue( '0' ) );
467
+ expect( onChange ).toHaveBeenCalledTimes( 3 );
468
+ onChange.mockClear();
469
+
470
+ const hueSlider = screen
471
+ .getAllByRole( 'slider', { name: 'Hue' } )
472
+ .at( -1 )!;
473
+ fireEvent.change( hueSlider, { target: { value: 90 } } );
474
+ expect( onChange ).not.toHaveBeenCalled();
475
+ } );
476
+
381
477
  it( 'should reset saturation to 0 when a mid-gray is entered via hex input', async () => {
382
478
  const user = userEvent.setup();
383
479
 
@@ -501,6 +597,412 @@ describe( 'ColorPicker', () => {
501
597
  } );
502
598
  } );
503
599
 
600
+ describe( 'visual picker', () => {
601
+ const mockInteractiveBounds = ( interactive: HTMLElement ) => {
602
+ interactive.getBoundingClientRect = () =>
603
+ ( {
604
+ left: 0,
605
+ top: 0,
606
+ width: 100,
607
+ height: 100,
608
+ right: 100,
609
+ bottom: 100,
610
+ x: 0,
611
+ y: 0,
612
+ toJSON: () => ( {} ),
613
+ } ) as DOMRect;
614
+ };
615
+
616
+ it( 'preserves saturation when keyboard-navigating to black in controlled mode', () => {
617
+ const onChange = jest.fn();
618
+
619
+ // Saturated red with mid brightness — HSVA s stays high at black.
620
+ const { container } = render(
621
+ <ControlledColorPicker
622
+ onChange={ onChange }
623
+ enableAlpha={ false }
624
+ initialColor="#cc0000"
625
+ />
626
+ );
627
+
628
+ const colorSlider = screen.getByRole( 'slider', { name: 'Color' } );
629
+ // Pointer is a presentational sibling; no accessible role.
630
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
631
+ const pointer = container.querySelector(
632
+ '.react-colorful__saturation-pointer'
633
+ ) as HTMLElement;
634
+ expect( pointer ).toBeTruthy();
635
+
636
+ const leftBefore = pointer.style.left;
637
+ expect( parseFloat( leftBefore ) ).toBeGreaterThan( 50 );
638
+
639
+ // react-colorful reads which/keyCode (37–40), not event.key.
640
+ // From v≈80, 5% steps need at least 16 ArrowDown presses to reach black.
641
+ colorSlider.focus();
642
+ for ( let i = 0; i < 20; i++ ) {
643
+ fireEvent.keyDown( colorSlider, {
644
+ key: 'ArrowDown',
645
+ keyCode: 40,
646
+ which: 40,
647
+ } );
648
+ }
649
+
650
+ expect( pointer ).toHaveStyle( { top: '100%', left: leftBefore } );
651
+ expect( onChange ).toHaveBeenLastCalledWith( '#000000' );
652
+ } );
653
+
654
+ it( 'preserves saturation when pointer selects black in controlled mode', () => {
655
+ const onChange = jest.fn();
656
+
657
+ const { container } = render(
658
+ <ControlledColorPicker
659
+ onChange={ onChange }
660
+ enableAlpha={ false }
661
+ initialColor="#cc0000"
662
+ />
663
+ );
664
+
665
+ const interactive = screen.getByRole( 'slider', { name: 'Color' } );
666
+ mockInteractiveBounds( interactive );
667
+
668
+ // Pointer is a presentational sibling; no accessible role.
669
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
670
+ const pointer = container.querySelector(
671
+ '.react-colorful__saturation-pointer'
672
+ ) as HTMLElement;
673
+ expect( parseFloat( pointer.style.left ) ).toBeGreaterThan( 50 );
674
+
675
+ // Click the bottom of the saturation surface (v = 0) at ~80% saturation.
676
+ fireEvent.mouseDown( interactive, {
677
+ buttons: 1,
678
+ pageX: 80,
679
+ pageY: 100,
680
+ clientX: 80,
681
+ clientY: 100,
682
+ } );
683
+
684
+ expect( pointer ).toHaveStyle( { top: '100%' } );
685
+ // Lossy HSVA→HSLA→HSVA sync would snap left to 0%.
686
+ expect( pointer ).not.toHaveStyle( { left: '0%' } );
687
+ expect( parseFloat( pointer.style.left ) ).toBeGreaterThan( 50 );
688
+ expect( onChange ).toHaveBeenLastCalledWith( '#000000' );
689
+ } );
690
+
691
+ it( 'preserves saturation after a full pointer drag to black then HSL hue edit', async () => {
692
+ const user = userEvent.setup();
693
+ const onChange = jest.fn();
694
+
695
+ const { container } = render(
696
+ <ControlledColorPicker
697
+ onChange={ onChange }
698
+ enableAlpha={ false }
699
+ initialColor="#cc0000"
700
+ />
701
+ );
702
+
703
+ const interactive = screen.getByRole( 'slider', { name: 'Color' } );
704
+ mockInteractiveBounds( interactive );
705
+
706
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
707
+ const pointer = container.querySelector(
708
+ '.react-colorful__saturation-pointer'
709
+ ) as HTMLElement;
710
+
711
+ // Full pointer lifecycle so isPointerInteractingRef is exercised
712
+ // and prevHslaRef stays in sync across mid-gesture parent updates.
713
+ fireEvent.pointerDown( interactive, {
714
+ pointerId: 1,
715
+ buttons: 1,
716
+ pageX: 80,
717
+ pageY: 50,
718
+ clientX: 80,
719
+ clientY: 50,
720
+ } );
721
+ fireEvent.mouseDown( interactive, {
722
+ buttons: 1,
723
+ pageX: 80,
724
+ pageY: 50,
725
+ clientX: 80,
726
+ clientY: 50,
727
+ } );
728
+ fireEvent.mouseMove( interactive, {
729
+ buttons: 1,
730
+ pageX: 80,
731
+ pageY: 100,
732
+ clientX: 80,
733
+ clientY: 100,
734
+ } );
735
+ fireEvent.pointerUp( interactive, {
736
+ pointerId: 1,
737
+ buttons: 0,
738
+ pageX: 80,
739
+ pageY: 100,
740
+ clientX: 80,
741
+ clientY: 100,
742
+ } );
743
+ fireEvent.mouseUp( interactive, {
744
+ buttons: 0,
745
+ pageX: 80,
746
+ pageY: 100,
747
+ clientX: 80,
748
+ clientY: 100,
749
+ } );
750
+
751
+ expect( pointer ).toHaveStyle( { top: '100%' } );
752
+ expect( parseFloat( pointer.style.left ) ).toBeGreaterThan( 50 );
753
+ expect( onChange ).toHaveBeenLastCalledWith( '#000000' );
754
+ const leftAfterDrag = pointer.style.left;
755
+
756
+ const formatSelector = screen.getByRole( 'combobox' );
757
+ await user.selectOptions( formatSelector, 'hsl' );
758
+
759
+ const hueSlider = screen
760
+ .getAllByRole( 'slider', { name: 'Hue' } )
761
+ .at( -1 )!;
762
+ fireEvent.change( hueSlider, { target: { value: 200 } } );
763
+ await waitFor( () => expect( hueSlider ).toHaveValue( '200' ) );
764
+
765
+ // Stale prevHslaRef would mistake the hue edit for a saturation
766
+ // change and snap the pointer to left: 0%.
767
+ expect( pointer ).toHaveStyle( {
768
+ top: '100%',
769
+ left: leftAfterDrag,
770
+ } );
771
+ } );
772
+
773
+ it( 'places the pointer at s:0 when HSL lightness is set to white', async () => {
774
+ const user = userEvent.setup();
775
+ const onChange = jest.fn();
776
+
777
+ const { container } = render(
778
+ <ControlledColorPicker
779
+ onChange={ onChange }
780
+ enableAlpha={ false }
781
+ initialColor="#cc0000"
782
+ />
783
+ );
784
+
785
+ const formatSelector = screen.getByRole( 'combobox' );
786
+ await user.selectOptions( formatSelector, 'hsl' );
787
+
788
+ const lightnessSlider = screen.getByRole( 'slider', {
789
+ name: 'Lightness',
790
+ } );
791
+ fireEvent.change( lightnessSlider, { target: { value: 100 } } );
792
+ await waitFor( () =>
793
+ expect( lightnessSlider ).toHaveValue( '100' )
794
+ );
795
+ expect( onChange ).toHaveBeenLastCalledWith( '#ffffff' );
796
+
797
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
798
+ const pointer = container.querySelector(
799
+ '.react-colorful__saturation-pointer'
800
+ ) as HTMLElement;
801
+ // Only v:100,s:0 is white; a preserved chromatic s leaves the
802
+ // pointer at top-right and makes the next key step jump to red.
803
+ expect( pointer ).toHaveStyle( { top: '0%', left: '0%' } );
804
+
805
+ const colorSlider = screen.getByRole( 'slider', { name: 'Color' } );
806
+ colorSlider.focus();
807
+ fireEvent.keyDown( colorSlider, {
808
+ key: 'ArrowDown',
809
+ keyCode: 40,
810
+ which: 40,
811
+ } );
812
+
813
+ expect( onChange ).not.toHaveBeenLastCalledWith( '#f50000' );
814
+ expect( onChange ).not.toHaveBeenLastCalledWith( '#cc0000' );
815
+ } );
816
+
817
+ it( 'preserves visual saturation when HSL hue is edited at black', async () => {
818
+ const user = userEvent.setup();
819
+
820
+ const { container } = render(
821
+ <ControlledColorPicker
822
+ enableAlpha={ false }
823
+ initialColor="#cc0000"
824
+ />
825
+ );
826
+
827
+ const formatSelector = screen.getByRole( 'combobox' );
828
+ await user.selectOptions( formatSelector, 'hsl' );
829
+
830
+ const lightnessSlider = screen.getByRole( 'slider', {
831
+ name: 'Lightness',
832
+ } );
833
+ fireEvent.change( lightnessSlider, { target: { value: 0 } } );
834
+ await waitFor( () => expect( lightnessSlider ).toHaveValue( '0' ) );
835
+
836
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
837
+ const pointer = container.querySelector(
838
+ '.react-colorful__saturation-pointer'
839
+ ) as HTMLElement;
840
+ const leftBefore = pointer.style.left;
841
+ expect( parseFloat( leftBefore ) ).toBeGreaterThan( 50 );
842
+ expect( pointer ).toHaveStyle( { top: '100%' } );
843
+
844
+ const hueSlider = screen
845
+ .getAllByRole( 'slider', { name: 'Hue' } )
846
+ .at( -1 )!;
847
+ fireEvent.change( hueSlider, { target: { value: 200 } } );
848
+ await waitFor( () => expect( hueSlider ).toHaveValue( '200' ) );
849
+
850
+ expect( pointer ).toHaveStyle( {
851
+ top: '100%',
852
+ left: leftBefore,
853
+ } );
854
+ } );
855
+
856
+ it( 'keeps the white visual coordinate at s:0 when HSL hue is edited', async () => {
857
+ const user = userEvent.setup();
858
+
859
+ const { container } = render(
860
+ <ControlledColorPicker
861
+ enableAlpha={ false }
862
+ initialColor="#cc0000"
863
+ />
864
+ );
865
+
866
+ const formatSelector = screen.getByRole( 'combobox' );
867
+ await user.selectOptions( formatSelector, 'hsl' );
868
+
869
+ const lightnessSlider = screen.getByRole( 'slider', {
870
+ name: 'Lightness',
871
+ } );
872
+ fireEvent.change( lightnessSlider, { target: { value: 100 } } );
873
+ await waitFor( () =>
874
+ expect( lightnessSlider ).toHaveValue( '100' )
875
+ );
876
+
877
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
878
+ const pointer = container.querySelector(
879
+ '.react-colorful__saturation-pointer'
880
+ ) as HTMLElement;
881
+ expect( pointer ).toHaveStyle( { top: '0%', left: '0%' } );
882
+
883
+ const hueSlider = screen
884
+ .getAllByRole( 'slider', { name: 'Hue' } )
885
+ .at( -1 )!;
886
+ fireEvent.change( hueSlider, { target: { value: 200 } } );
887
+ await waitFor( () => expect( hueSlider ).toHaveValue( '200' ) );
888
+
889
+ expect( pointer ).toHaveStyle( { top: '0%', left: '0%' } );
890
+ } );
891
+
892
+ it( 'uses the HSL hue when leaving white through the visual surface', async () => {
893
+ const user = userEvent.setup();
894
+ const onChange = jest.fn();
895
+
896
+ render(
897
+ <ControlledColorPicker
898
+ onChange={ onChange }
899
+ enableAlpha={ false }
900
+ initialColor="#cc0000"
901
+ />
902
+ );
903
+
904
+ await user.selectOptions( screen.getByRole( 'combobox' ), 'hsl' );
905
+ const lightnessSlider = screen.getByRole( 'slider', {
906
+ name: 'Lightness',
907
+ } );
908
+ fireEvent.change( lightnessSlider, { target: { value: 100 } } );
909
+
910
+ const hueSlider = screen
911
+ .getAllByRole( 'slider', { name: 'Hue' } )
912
+ .at( -1 )!;
913
+ fireEvent.change( hueSlider, { target: { value: 200 } } );
914
+ await waitFor( () => expect( hueSlider ).toHaveValue( '200' ) );
915
+
916
+ const colorSlider = screen.getByRole( 'slider', { name: 'Color' } );
917
+ mockInteractiveBounds( colorSlider );
918
+ onChange.mockClear();
919
+ fireEvent.mouseDown( colorSlider, {
920
+ buttons: 1,
921
+ pageX: 50,
922
+ pageY: 0,
923
+ clientX: 50,
924
+ clientY: 0,
925
+ } );
926
+
927
+ expect( onChange ).toHaveBeenLastCalledWith( '#80d4ff' );
928
+ } );
929
+
930
+ it( 'uses the HSL hue when leaving a mid-gray through the visual surface', async () => {
931
+ const user = userEvent.setup();
932
+ const onChange = jest.fn();
933
+
934
+ render(
935
+ <ControlledColorPicker
936
+ onChange={ onChange }
937
+ enableAlpha={ false }
938
+ initialColor="#808080"
939
+ />
940
+ );
941
+
942
+ await user.selectOptions( screen.getByRole( 'combobox' ), 'hsl' );
943
+ const hueSlider = screen
944
+ .getAllByRole( 'slider', { name: 'Hue' } )
945
+ .at( -1 )!;
946
+ fireEvent.change( hueSlider, { target: { value: 200 } } );
947
+ await waitFor( () => expect( hueSlider ).toHaveValue( '200' ) );
948
+
949
+ const colorSlider = screen.getByRole( 'slider', { name: 'Color' } );
950
+ mockInteractiveBounds( colorSlider );
951
+ onChange.mockClear();
952
+ fireEvent.mouseDown( colorSlider, {
953
+ buttons: 1,
954
+ pageX: 50,
955
+ pageY: 50,
956
+ clientX: 50,
957
+ clientY: 50,
958
+ } );
959
+
960
+ expect( onChange ).toHaveBeenLastCalledWith( '#416c81' );
961
+ } );
962
+
963
+ it( 'updates visual saturation when HSL saturation is edited at black', async () => {
964
+ const user = userEvent.setup();
965
+
966
+ const { container } = render(
967
+ <ControlledColorPicker
968
+ enableAlpha={ false }
969
+ initialColor="#cc0000"
970
+ />
971
+ );
972
+
973
+ const formatSelector = screen.getByRole( 'combobox' );
974
+ await user.selectOptions( formatSelector, 'hsl' );
975
+
976
+ const lightnessSlider = screen.getByRole( 'slider', {
977
+ name: 'Lightness',
978
+ } );
979
+ fireEvent.change( lightnessSlider, { target: { value: 0 } } );
980
+ await waitFor( () => expect( lightnessSlider ).toHaveValue( '0' ) );
981
+
982
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
983
+ const pointer = container.querySelector(
984
+ '.react-colorful__saturation-pointer'
985
+ ) as HTMLElement;
986
+ expect( pointer ).toHaveStyle( { top: '100%' } );
987
+ expect( parseFloat( pointer.style.left ) ).toBeGreaterThan( 50 );
988
+
989
+ const saturationSlider = screen.getByRole( 'slider', {
990
+ name: 'Saturation',
991
+ } );
992
+ fireEvent.change( saturationSlider, {
993
+ target: { value: 25 },
994
+ } );
995
+ await waitFor( () =>
996
+ expect( saturationSlider ).toHaveValue( '25' )
997
+ );
998
+
999
+ expect( pointer ).toHaveStyle( {
1000
+ top: '100%',
1001
+ left: '25%',
1002
+ } );
1003
+ } );
1004
+ } );
1005
+
504
1006
  describe.each( [
505
1007
  [ 'hsl', 'HSL' ],
506
1008
  [ 'rgb', 'RGB' ],
@@ -59,6 +59,8 @@ export interface PickerProps {
59
59
  hsla: HslaColor;
60
60
  enableAlpha: boolean;
61
61
  onChange: ( nextHsla: HslaColor ) => void;
62
+ onInteractionStart?: () => void;
63
+ onInteractionEnd?: () => void;
62
64
  }
63
65
 
64
66
  export interface ColorInputProps {