@wordpress/components 32.5.0 → 32.6.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 (282) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +18 -4
  4. package/build/alignment-matrix-control/cell.cjs +3 -3
  5. package/build/alignment-matrix-control/cell.cjs.map +2 -2
  6. package/build/alignment-matrix-control/index.cjs +3 -3
  7. package/build/alignment-matrix-control/index.cjs.map +2 -2
  8. package/build/autocomplete/autocompleter-ui.cjs +75 -79
  9. package/build/autocomplete/autocompleter-ui.cjs.map +2 -2
  10. package/build/autocomplete/get-autocomplete-match.cjs +91 -0
  11. package/build/autocomplete/get-autocomplete-match.cjs.map +7 -0
  12. package/build/autocomplete/index.cjs +104 -107
  13. package/build/autocomplete/index.cjs.map +3 -3
  14. package/build/box-control/index.cjs +0 -8
  15. package/build/box-control/index.cjs.map +2 -2
  16. package/build/box-control/utils.cjs +1 -10
  17. package/build/box-control/utils.cjs.map +2 -2
  18. package/build/calendar/utils/use-localization-props.cjs +3 -2
  19. package/build/calendar/utils/use-localization-props.cjs.map +2 -2
  20. package/build/custom-gradient-picker/index.cjs.map +2 -2
  21. package/build/custom-select-control/index.cjs.map +3 -3
  22. package/build/custom-select-control-v2/custom-select.cjs +2 -2
  23. package/build/custom-select-control-v2/custom-select.cjs.map +2 -2
  24. package/build/custom-select-control-v2/index.cjs.map +3 -3
  25. package/build/date-time/{date → date-picker}/index.cjs +6 -6
  26. package/build/date-time/{date → date-picker}/index.cjs.map +2 -2
  27. package/build/date-time/{date → date-picker}/styles.cjs +17 -17
  28. package/build/date-time/{date → date-picker}/styles.cjs.map +2 -2
  29. package/build/date-time/{date → date-picker}/use-lilius/index.cjs +1 -1
  30. package/build/date-time/{date → date-picker}/use-lilius/index.cjs.map +1 -1
  31. package/build/date-time/date-time/index.cjs +6 -6
  32. package/build/date-time/date-time/index.cjs.map +2 -2
  33. package/build/date-time/index.cjs +4 -4
  34. package/build/date-time/index.cjs.map +2 -2
  35. package/build/date-time/{time → time-picker}/index.cjs +6 -6
  36. package/build/date-time/time-picker/index.cjs.map +7 -0
  37. package/build/date-time/{time → time-picker}/styles.cjs +21 -21
  38. package/build/date-time/{time → time-picker}/styles.cjs.map +2 -2
  39. package/build/date-time/{time → time-picker}/time-input/index.cjs +1 -1
  40. package/build/date-time/{time → time-picker}/time-input/index.cjs.map +1 -1
  41. package/build/date-time/{time → time-picker}/timezone.cjs +1 -1
  42. package/build/date-time/{time → time-picker}/timezone.cjs.map +1 -1
  43. package/build/modal/index.cjs.map +2 -2
  44. package/build/palette-edit/index.cjs.map +2 -2
  45. package/build/radio-control/index.cjs +2 -0
  46. package/build/radio-control/index.cjs.map +2 -2
  47. package/build/sandbox/index.cjs +127 -3
  48. package/build/sandbox/index.cjs.map +2 -2
  49. package/build/textarea-control/styles/textarea-control-styles.cjs +3 -3
  50. package/build/textarea-control/styles/textarea-control-styles.cjs.map +2 -2
  51. package/build/validated-form-controls/control-with-error.cjs +12 -8
  52. package/build/validated-form-controls/control-with-error.cjs.map +2 -2
  53. package/build-module/alignment-matrix-control/cell.mjs +3 -3
  54. package/build-module/alignment-matrix-control/cell.mjs.map +2 -2
  55. package/build-module/alignment-matrix-control/index.mjs +3 -3
  56. package/build-module/alignment-matrix-control/index.mjs.map +2 -2
  57. package/build-module/autocomplete/autocompleter-ui.mjs +74 -78
  58. package/build-module/autocomplete/autocompleter-ui.mjs.map +2 -2
  59. package/build-module/autocomplete/get-autocomplete-match.mjs +56 -0
  60. package/build-module/autocomplete/get-autocomplete-match.mjs.map +7 -0
  61. package/build-module/autocomplete/index.mjs +103 -107
  62. package/build-module/autocomplete/index.mjs.map +3 -3
  63. package/build-module/box-control/index.mjs +1 -9
  64. package/build-module/box-control/index.mjs.map +2 -2
  65. package/build-module/box-control/utils.mjs +1 -9
  66. package/build-module/box-control/utils.mjs.map +2 -2
  67. package/build-module/calendar/utils/use-localization-props.mjs +3 -2
  68. package/build-module/calendar/utils/use-localization-props.mjs.map +2 -2
  69. package/build-module/custom-gradient-picker/index.mjs.map +2 -2
  70. package/build-module/custom-select-control/index.mjs +2 -2
  71. package/build-module/custom-select-control/index.mjs.map +2 -2
  72. package/build-module/custom-select-control-v2/custom-select.mjs +2 -2
  73. package/build-module/custom-select-control-v2/custom-select.mjs.map +2 -2
  74. package/build-module/custom-select-control-v2/index.mjs +2 -2
  75. package/build-module/custom-select-control-v2/index.mjs.map +2 -2
  76. package/build-module/date-time/{date → date-picker}/index.mjs +3 -3
  77. package/build-module/date-time/{date → date-picker}/index.mjs.map +2 -2
  78. package/build-module/date-time/{date → date-picker}/styles.mjs +17 -17
  79. package/build-module/date-time/{date → date-picker}/styles.mjs.map +2 -2
  80. package/build-module/date-time/{date → date-picker}/use-lilius/index.mjs +1 -1
  81. package/build-module/date-time/{date → date-picker}/use-lilius/index.mjs.map +1 -1
  82. package/build-module/date-time/date-time/index.mjs +2 -2
  83. package/build-module/date-time/date-time/index.mjs.map +1 -1
  84. package/build-module/date-time/index.mjs +2 -2
  85. package/build-module/date-time/index.mjs.map +1 -1
  86. package/build-module/date-time/{time → time-picker}/index.mjs +3 -3
  87. package/build-module/date-time/time-picker/index.mjs.map +7 -0
  88. package/build-module/date-time/{time → time-picker}/styles.mjs +21 -21
  89. package/build-module/date-time/{time → time-picker}/styles.mjs.map +2 -2
  90. package/build-module/date-time/{time → time-picker}/time-input/index.mjs +1 -1
  91. package/build-module/date-time/{time → time-picker}/time-input/index.mjs.map +1 -1
  92. package/build-module/date-time/{time → time-picker}/timezone.mjs +1 -1
  93. package/build-module/date-time/{time → time-picker}/timezone.mjs.map +1 -1
  94. package/build-module/modal/index.mjs.map +2 -2
  95. package/build-module/palette-edit/index.mjs.map +2 -2
  96. package/build-module/radio-control/index.mjs +2 -0
  97. package/build-module/radio-control/index.mjs.map +2 -2
  98. package/build-module/sandbox/index.mjs +128 -4
  99. package/build-module/sandbox/index.mjs.map +2 -2
  100. package/build-module/textarea-control/styles/textarea-control-styles.mjs +3 -3
  101. package/build-module/textarea-control/styles/textarea-control-styles.mjs.map +2 -2
  102. package/build-module/validated-form-controls/control-with-error.mjs +12 -8
  103. package/build-module/validated-form-controls/control-with-error.mjs.map +2 -2
  104. package/build-style/style-rtl.css +83 -26
  105. package/build-style/style.css +83 -26
  106. package/build-types/autocomplete/autocompleter-ui.d.ts +2 -2
  107. package/build-types/autocomplete/autocompleter-ui.d.ts.map +1 -1
  108. package/build-types/autocomplete/get-autocomplete-match.d.ts +11 -0
  109. package/build-types/autocomplete/get-autocomplete-match.d.ts.map +1 -0
  110. package/build-types/autocomplete/index.d.ts +8 -0
  111. package/build-types/autocomplete/index.d.ts.map +1 -1
  112. package/build-types/autocomplete/test/get-autocomplete-match.d.ts +2 -0
  113. package/build-types/autocomplete/test/get-autocomplete-match.d.ts.map +1 -0
  114. package/build-types/autocomplete/types.d.ts +23 -9
  115. package/build-types/autocomplete/types.d.ts.map +1 -1
  116. package/build-types/box-control/index.d.ts.map +1 -1
  117. package/build-types/box-control/utils.d.ts +7 -16
  118. package/build-types/box-control/utils.d.ts.map +1 -1
  119. package/build-types/button/stories/index.story.d.ts +0 -1
  120. package/build-types/button/stories/index.story.d.ts.map +1 -1
  121. package/build-types/calendar/utils/use-localization-props.d.ts +3 -3
  122. package/build-types/calendar/utils/use-localization-props.d.ts.map +1 -1
  123. package/build-types/checkbox-control/types.d.ts +4 -0
  124. package/build-types/checkbox-control/types.d.ts.map +1 -1
  125. package/build-types/custom-gradient-picker/constants.d.ts +2 -2
  126. package/build-types/custom-gradient-picker/index.d.ts.map +1 -1
  127. package/build-types/custom-select-control-v2/custom-select.d.ts +3 -3
  128. package/build-types/custom-select-control-v2/custom-select.d.ts.map +1 -1
  129. package/build-types/custom-select-control-v2/types.d.ts +1 -1
  130. package/build-types/custom-select-control-v2/types.d.ts.map +1 -1
  131. package/build-types/date-time/date-picker/index.d.ts.map +1 -0
  132. package/build-types/date-time/date-picker/styles.d.ts.map +1 -0
  133. package/build-types/date-time/date-picker/test/index.d.ts.map +1 -0
  134. package/build-types/date-time/date-picker/test/use-lilius.d.ts.map +1 -0
  135. package/build-types/date-time/date-picker/use-lilius/index.d.ts.map +1 -0
  136. package/build-types/date-time/date-time/index.d.ts +2 -2
  137. package/build-types/date-time/date-time/index.d.ts.map +1 -1
  138. package/build-types/date-time/index.d.ts +2 -2
  139. package/build-types/date-time/index.d.ts.map +1 -1
  140. package/build-types/date-time/stories/date.story.d.ts +1 -1
  141. package/build-types/date-time/stories/date.story.d.ts.map +1 -1
  142. package/build-types/date-time/stories/time.story.d.ts +1 -1
  143. package/build-types/date-time/stories/time.story.d.ts.map +1 -1
  144. package/build-types/date-time/{time → time-picker}/index.d.ts +1 -1
  145. package/build-types/date-time/time-picker/index.d.ts.map +1 -0
  146. package/build-types/date-time/time-picker/styles.d.ts.map +1 -0
  147. package/build-types/date-time/time-picker/test/index.d.ts.map +1 -0
  148. package/build-types/date-time/time-picker/time-input/index.d.ts.map +1 -0
  149. package/build-types/date-time/time-picker/time-input/test/index.d.ts.map +1 -0
  150. package/build-types/date-time/time-picker/timezone.d.ts.map +1 -0
  151. package/build-types/date-time/types.d.ts +1 -1
  152. package/build-types/date-time/types.d.ts.map +1 -1
  153. package/build-types/font-size-picker/constants.d.ts +2 -2
  154. package/build-types/font-size-picker/constants.d.ts.map +1 -1
  155. package/build-types/modal/index.d.ts.map +1 -1
  156. package/build-types/palette-edit/index.d.ts +1 -1
  157. package/build-types/palette-edit/index.d.ts.map +1 -1
  158. package/build-types/radio-control/index.d.ts.map +1 -1
  159. package/build-types/radio-control/types.d.ts +6 -0
  160. package/build-types/radio-control/types.d.ts.map +1 -1
  161. package/build-types/sandbox/index.d.ts +1 -1
  162. package/build-types/sandbox/index.d.ts.map +1 -1
  163. package/build-types/sandbox/types.d.ts +11 -0
  164. package/build-types/sandbox/types.d.ts.map +1 -1
  165. package/build-types/textarea-control/stories/index.story.d.ts.map +1 -1
  166. package/build-types/textarea-control/styles/textarea-control-styles.d.ts.map +1 -1
  167. package/build-types/validated-form-controls/components/checkbox-control.d.ts +2 -1
  168. package/build-types/validated-form-controls/components/checkbox-control.d.ts.map +1 -1
  169. package/build-types/validated-form-controls/components/radio-control.d.ts +2 -1
  170. package/build-types/validated-form-controls/components/radio-control.d.ts.map +1 -1
  171. package/build-types/validated-form-controls/control-with-error.d.ts.map +1 -1
  172. package/package.json +21 -21
  173. package/src/alignment-matrix-control/README.md +1 -1
  174. package/src/alignment-matrix-control/style.module.scss +1 -1
  175. package/src/angle-picker-control/style.module.scss +1 -0
  176. package/src/autocomplete/README.md +2 -2
  177. package/src/autocomplete/autocompleter-ui.native.js +166 -173
  178. package/src/autocomplete/autocompleter-ui.tsx +114 -116
  179. package/src/autocomplete/get-autocomplete-match.ts +115 -0
  180. package/src/autocomplete/index.tsx +129 -208
  181. package/src/autocomplete/test/get-autocomplete-match.ts +338 -0
  182. package/src/autocomplete/test/index.tsx +112 -4
  183. package/src/autocomplete/types.ts +17 -10
  184. package/src/box-control/index.tsx +1 -19
  185. package/src/box-control/utils.ts +1 -19
  186. package/src/button/README.md +1 -1
  187. package/src/button/stories/index.story.tsx +0 -1
  188. package/src/button/style.scss +1 -7
  189. package/src/calendar/style.scss +3 -3
  190. package/src/calendar/utils/use-localization-props.ts +3 -4
  191. package/src/checkbox-control/style.scss +17 -5
  192. package/src/checkbox-control/types.ts +4 -0
  193. package/src/circular-option-picker/style.scss +1 -1
  194. package/src/color-palette/style.scss +1 -1
  195. package/src/css.d.ts +1 -0
  196. package/src/custom-gradient-picker/index.tsx +1 -0
  197. package/src/custom-select-control/index.tsx +3 -3
  198. package/src/custom-select-control-v2/custom-select.tsx +4 -4
  199. package/src/custom-select-control-v2/index.tsx +2 -2
  200. package/src/custom-select-control-v2/types.ts +1 -1
  201. package/src/date-time/README.md +3 -3
  202. package/src/date-time/date-picker/README.md +65 -0
  203. package/src/date-time/date-time/index.tsx +2 -2
  204. package/src/date-time/index.ts +2 -2
  205. package/src/date-time/stories/date.story.tsx +1 -1
  206. package/src/date-time/stories/time.story.tsx +1 -1
  207. package/src/date-time/time-picker/README.md +119 -0
  208. package/src/date-time/{time → time-picker}/index.tsx +1 -1
  209. package/src/date-time/types.ts +1 -1
  210. package/src/divider/README.md +5 -6
  211. package/src/dropdown-menu/style.scss +1 -1
  212. package/src/flex/stories/index.story.tsx +1 -1
  213. package/src/form-file-upload/README.md +3 -3
  214. package/src/form-toggle/style.scss +35 -2
  215. package/src/form-token-field/style.scss +12 -3
  216. package/src/gradient-picker/README.md +2 -2
  217. package/src/h-stack/README.md +10 -15
  218. package/src/h-stack/stories/index.story.tsx +2 -2
  219. package/src/heading/stories/index.story.tsx +1 -1
  220. package/src/higher-order/with-focus-outside/index.native.js +21 -20
  221. package/src/icon/README.md +1 -1
  222. package/src/menu/README.md +2 -2
  223. package/src/mobile/utils/get-px-from-css-unit.native.js +1 -1
  224. package/src/modal/index.tsx +1 -0
  225. package/src/palette-edit/index.tsx +1 -0
  226. package/src/radio-control/index.tsx +2 -0
  227. package/src/radio-control/style.scss +21 -2
  228. package/src/radio-control/test/index.tsx +10 -0
  229. package/src/radio-control/types.ts +6 -0
  230. package/src/sandbox/index.native.js +2 -2
  231. package/src/sandbox/index.tsx +191 -11
  232. package/src/sandbox/test/index.tsx +65 -24
  233. package/src/sandbox/types.ts +11 -0
  234. package/src/snackbar/style.scss +2 -2
  235. package/src/tab-panel/style.scss +1 -1
  236. package/src/tabs/README.md +6 -6
  237. package/src/text/stories/index.story.tsx +1 -1
  238. package/src/textarea-control/stories/index.story.tsx +3 -0
  239. package/src/textarea-control/styles/textarea-control-styles.ts +6 -0
  240. package/src/toggle-control/style.scss +1 -1
  241. package/src/toggle-control/test/index.tsx +8 -2
  242. package/src/toolbar/toolbar-button/toolbar-button-container.native.js +3 -1
  243. package/src/tree-select/README.md +1 -1
  244. package/src/v-stack/README.md +10 -15
  245. package/src/v-stack/stories/index.story.tsx +2 -2
  246. package/src/validated-form-controls/control-with-error.tsx +17 -12
  247. package/src/validated-form-controls/test/control-with-error.tsx +28 -1
  248. package/src/view/README.md +2 -5
  249. package/build/date-time/time/index.cjs.map +0 -7
  250. package/build-module/date-time/time/index.mjs.map +0 -7
  251. package/build-types/date-time/date/index.d.ts.map +0 -1
  252. package/build-types/date-time/date/styles.d.ts.map +0 -1
  253. package/build-types/date-time/date/test/index.d.ts.map +0 -1
  254. package/build-types/date-time/date/test/use-lilius.d.ts.map +0 -1
  255. package/build-types/date-time/date/use-lilius/index.d.ts.map +0 -1
  256. package/build-types/date-time/time/index.d.ts.map +0 -1
  257. package/build-types/date-time/time/styles.d.ts.map +0 -1
  258. package/build-types/date-time/time/test/index.d.ts.map +0 -1
  259. package/build-types/date-time/time/time-input/index.d.ts.map +0 -1
  260. package/build-types/date-time/time/time-input/test/index.d.ts.map +0 -1
  261. package/build-types/date-time/time/timezone.d.ts.map +0 -1
  262. package/src/button/stories/style.css +0 -8
  263. /package/build-types/date-time/{date → date-picker}/index.d.ts +0 -0
  264. /package/build-types/date-time/{date → date-picker}/styles.d.ts +0 -0
  265. /package/build-types/date-time/{date → date-picker}/test/index.d.ts +0 -0
  266. /package/build-types/date-time/{date → date-picker}/test/use-lilius.d.ts +0 -0
  267. /package/build-types/date-time/{date → date-picker}/use-lilius/index.d.ts +0 -0
  268. /package/build-types/date-time/{time → time-picker}/styles.d.ts +0 -0
  269. /package/build-types/date-time/{time → time-picker}/test/index.d.ts +0 -0
  270. /package/build-types/date-time/{time → time-picker}/time-input/index.d.ts +0 -0
  271. /package/build-types/date-time/{time → time-picker}/time-input/test/index.d.ts +0 -0
  272. /package/build-types/date-time/{time → time-picker}/timezone.d.ts +0 -0
  273. /package/src/date-time/{date → date-picker}/index.tsx +0 -0
  274. /package/src/date-time/{date → date-picker}/styles.ts +0 -0
  275. /package/src/date-time/{date → date-picker}/test/index.tsx +0 -0
  276. /package/src/date-time/{date → date-picker}/test/use-lilius.ts +0 -0
  277. /package/src/date-time/{date → date-picker}/use-lilius/index.ts +0 -0
  278. /package/src/date-time/{time → time-picker}/styles.ts +0 -0
  279. /package/src/date-time/{time → time-picker}/test/index.tsx +0 -0
  280. /package/src/date-time/{time → time-picker}/time-input/index.tsx +0 -0
  281. /package/src/date-time/{time → time-picker}/time-input/test/index.tsx +0 -0
  282. /package/src/date-time/{time → time-picker}/timezone.tsx +0 -0
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { getAutocompleteMatch } from '../get-autocomplete-match';
5
+ import type { WPCompleter } from '../types';
6
+
7
+ const createCompleter = (
8
+ overrides: Partial< WPCompleter > = {}
9
+ ): WPCompleter => ( {
10
+ name: 'test',
11
+ triggerPrefix: '/',
12
+ options: [],
13
+ getOptionLabel: ( option: any ) => option,
14
+ ...overrides,
15
+ } );
16
+
17
+ describe( 'getAutocompleteMatch', () => {
18
+ it( 'should return null for empty text content', () => {
19
+ const completers = [ createCompleter() ];
20
+ expect(
21
+ getAutocompleteMatch( '', completers, 0, false, () => '' )
22
+ ).toBeNull();
23
+ } );
24
+
25
+ it( 'should return null when no completers are provided', () => {
26
+ expect(
27
+ getAutocompleteMatch( 'some text /', [], 0, false, () => '' )
28
+ ).toBeNull();
29
+ } );
30
+
31
+ it( 'should return null when trigger prefix is not found in text', () => {
32
+ const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
33
+ expect(
34
+ getAutocompleteMatch(
35
+ 'no trigger here',
36
+ completers,
37
+ 1,
38
+ false,
39
+ () => ''
40
+ )
41
+ ).toBeNull();
42
+ } );
43
+
44
+ it( 'should match a simple trigger prefix', () => {
45
+ const completers = [ createCompleter( { triggerPrefix: '/' } ) ];
46
+ const result = getAutocompleteMatch(
47
+ 'some text /query',
48
+ completers,
49
+ 1,
50
+ false,
51
+ () => ''
52
+ );
53
+ expect( result ).toEqual( {
54
+ completer: completers[ 0 ],
55
+ filterValue: 'query',
56
+ } );
57
+ } );
58
+
59
+ it( 'should return empty filterValue when only trigger is typed', () => {
60
+ const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
61
+ const result = getAutocompleteMatch(
62
+ 'hello @',
63
+ completers,
64
+ 1,
65
+ false,
66
+ () => ''
67
+ );
68
+ expect( result ).toEqual( {
69
+ completer: completers[ 0 ],
70
+ filterValue: '',
71
+ } );
72
+ } );
73
+
74
+ it( 'should prefer the rightmost matching trigger when multiple completers match', () => {
75
+ const slashCompleter = createCompleter( {
76
+ name: 'slash',
77
+ triggerPrefix: '/',
78
+ } );
79
+ const atCompleter = createCompleter( {
80
+ name: 'at',
81
+ triggerPrefix: '@',
82
+ } );
83
+ const result = getAutocompleteMatch(
84
+ '/command some text @user',
85
+ [ slashCompleter, atCompleter ],
86
+ 1,
87
+ false,
88
+ () => ''
89
+ );
90
+ expect( result?.completer.name ).toBe( 'at' );
91
+ } );
92
+
93
+ it( 'should return null when text after trigger is too long (>50 chars)', () => {
94
+ const completers = [ createCompleter( { triggerPrefix: '/' } ) ];
95
+ const longText = '/' + 'a'.repeat( 51 );
96
+ expect(
97
+ getAutocompleteMatch( longText, completers, 1, false, () => '' )
98
+ ).toBeNull();
99
+ } );
100
+
101
+ it( 'should match when text after trigger is exactly 50 chars', () => {
102
+ const completers = [ createCompleter( { triggerPrefix: '/' } ) ];
103
+ const text = '/' + 'a'.repeat( 50 );
104
+ const result = getAutocompleteMatch(
105
+ text,
106
+ completers,
107
+ 1,
108
+ false,
109
+ () => ''
110
+ );
111
+ expect( result ).not.toBeNull();
112
+ expect( result?.filterValue ).toBe( 'a'.repeat( 50 ) );
113
+ } );
114
+
115
+ it( 'should return null on mismatch with multiple words and no backspacing', () => {
116
+ const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
117
+ // 4 words from trigger, mismatch (filteredOptionsLength=0), not backspacing
118
+ expect(
119
+ getAutocompleteMatch(
120
+ 'text @one two three four',
121
+ completers,
122
+ 0,
123
+ false,
124
+ () => ''
125
+ )
126
+ ).toBeNull();
127
+ } );
128
+
129
+ it( 'should still match on mismatch when there is only one trigger word', () => {
130
+ const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
131
+ const result = getAutocompleteMatch(
132
+ 'text @xyz',
133
+ completers,
134
+ 0,
135
+ false,
136
+ () => ''
137
+ );
138
+ expect( result ).not.toBeNull();
139
+ expect( result?.filterValue ).toBe( 'xyz' );
140
+ } );
141
+
142
+ it( 'should allow matching while backspacing within 3 words of trigger', () => {
143
+ const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
144
+ const result = getAutocompleteMatch(
145
+ 'text @one two three',
146
+ completers,
147
+ 0,
148
+ true,
149
+ () => ''
150
+ );
151
+ expect( result ).not.toBeNull();
152
+ } );
153
+
154
+ it( 'should NOT match while backspacing if more than 3 words from trigger', () => {
155
+ const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
156
+ expect(
157
+ getAutocompleteMatch(
158
+ 'text @one two three four',
159
+ completers,
160
+ 0,
161
+ true,
162
+ () => ''
163
+ )
164
+ ).toBeNull();
165
+ } );
166
+
167
+ it( 'should return null when text after trigger starts with whitespace', () => {
168
+ const completers = [ createCompleter( { triggerPrefix: '/' } ) ];
169
+ expect(
170
+ getAutocompleteMatch( '/ query', completers, 1, false, () => '' )
171
+ ).toBeNull();
172
+ } );
173
+
174
+ it( 'should return null when text after trigger ends with multiple spaces', () => {
175
+ const completers = [ createCompleter( { triggerPrefix: '/' } ) ];
176
+ expect(
177
+ getAutocompleteMatch( '/query ', completers, 1, false, () => '' )
178
+ ).toBeNull();
179
+ } );
180
+
181
+ it( 'should respect allowContext returning false', () => {
182
+ const completers = [
183
+ createCompleter( {
184
+ triggerPrefix: '@',
185
+ allowContext: () => false,
186
+ } ),
187
+ ];
188
+ expect(
189
+ getAutocompleteMatch( 'text @user', completers, 1, false, () => '' )
190
+ ).toBeNull();
191
+ } );
192
+
193
+ it( 'should pass correct before/after text to allowContext', () => {
194
+ const allowContext = jest.fn().mockReturnValue( true );
195
+ const completers = [
196
+ createCompleter( {
197
+ triggerPrefix: '@',
198
+ allowContext,
199
+ } ),
200
+ ];
201
+ getAutocompleteMatch(
202
+ 'before @user',
203
+ completers,
204
+ 1,
205
+ false,
206
+ () => 'after'
207
+ );
208
+ expect( allowContext ).toHaveBeenCalledWith( 'before ', 'after' );
209
+ } );
210
+
211
+ it( 'should handle accented characters in filter value', () => {
212
+ const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
213
+ const result = getAutocompleteMatch(
214
+ 'text @café',
215
+ completers,
216
+ 1,
217
+ false,
218
+ () => ''
219
+ );
220
+ expect( result ).not.toBeNull();
221
+ expect( result?.filterValue ).toBe( 'cafe' );
222
+ } );
223
+
224
+ it( 'should match the longer trigger when prefixes overlap', () => {
225
+ const singleAt = createCompleter( {
226
+ name: 'single',
227
+ triggerPrefix: '@',
228
+ } );
229
+ const doubleAt = createCompleter( {
230
+ name: 'double',
231
+ triggerPrefix: '@@',
232
+ } );
233
+ const result = getAutocompleteMatch(
234
+ '@@user',
235
+ [ singleAt, doubleAt ],
236
+ 1,
237
+ false,
238
+ () => ''
239
+ );
240
+ expect( result?.completer.name ).toBe( 'double' );
241
+ expect( result?.filterValue ).toBe( 'user' );
242
+ } );
243
+
244
+ it( 'should match the shorter trigger when only it is present', () => {
245
+ const singleAt = createCompleter( {
246
+ name: 'single',
247
+ triggerPrefix: '@',
248
+ } );
249
+ const doubleAt = createCompleter( {
250
+ name: 'double',
251
+ triggerPrefix: '@@',
252
+ } );
253
+ const result = getAutocompleteMatch(
254
+ 'hello @user',
255
+ [ singleAt, doubleAt ],
256
+ 1,
257
+ false,
258
+ () => ''
259
+ );
260
+ expect( result?.completer.name ).toBe( 'single' );
261
+ expect( result?.filterValue ).toBe( 'user' );
262
+ } );
263
+
264
+ it( 'should handle special regex characters in trigger prefix', () => {
265
+ const completers = [ createCompleter( { triggerPrefix: '$$' } ) ];
266
+ const result = getAutocompleteMatch(
267
+ 'text $$query',
268
+ completers,
269
+ 1,
270
+ false,
271
+ () => ''
272
+ );
273
+ expect( result ).not.toBeNull();
274
+ expect( result?.filterValue ).toBe( 'query' );
275
+ } );
276
+
277
+ it( 'should match with spaces in filter value (single space)', () => {
278
+ const completers = [ createCompleter( { triggerPrefix: '/' } ) ];
279
+ const result = getAutocompleteMatch(
280
+ '/hello world',
281
+ completers,
282
+ 1,
283
+ false,
284
+ () => ''
285
+ );
286
+ expect( result ).not.toBeNull();
287
+ expect( result?.filterValue ).toBe( 'hello world' );
288
+ } );
289
+
290
+ it.each( [
291
+ {
292
+ text: 'café @user',
293
+ trigger: '@',
294
+ expected: 'user',
295
+ desc: 'accented text before trigger',
296
+ },
297
+ {
298
+ text: 'naïve /command',
299
+ trigger: '/',
300
+ expected: 'command',
301
+ desc: 'accented text before trigger (diaeresis)',
302
+ },
303
+ {
304
+ text: 'résumé @josé',
305
+ trigger: '@',
306
+ expected: 'jose',
307
+ desc: 'accents both before and after trigger',
308
+ },
309
+ {
310
+ text: '@café',
311
+ trigger: '@',
312
+ expected: 'cafe',
313
+ desc: 'accented text after trigger only',
314
+ },
315
+ {
316
+ text: 'a /héllo wörld',
317
+ trigger: '/',
318
+ expected: 'hello world',
319
+ desc: 'accented multi-word filter value',
320
+ },
321
+ ] )(
322
+ 'should handle accents correctly: $desc',
323
+ ( { text, trigger, expected } ) => {
324
+ const completers = [
325
+ createCompleter( { triggerPrefix: trigger } ),
326
+ ];
327
+ const result = getAutocompleteMatch(
328
+ text,
329
+ completers,
330
+ 1,
331
+ false,
332
+ () => ''
333
+ );
334
+ expect( result ).not.toBeNull();
335
+ expect( result?.filterValue ).toBe( expected );
336
+ }
337
+ );
338
+ } );
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { render, screen } from '@testing-library/react';
4
+ import { render, screen, renderHook } from '@testing-library/react';
5
5
  import userEvent from '@testing-library/user-event';
6
6
 
7
7
  /**
@@ -12,10 +12,119 @@ import { useRef } from '@wordpress/element';
12
12
  /**
13
13
  * Internal dependencies
14
14
  */
15
- import { getAutoCompleterUI } from '../autocompleter-ui';
15
+ import { AutocompleterUI } from '../autocompleter-ui';
16
+ import { useLastDifferentValue } from '..';
16
17
 
17
18
  type FruitOption = { visual: string; name: string; id: number };
18
19
 
20
+ function makeRecord( text: string ) {
21
+ return {
22
+ text,
23
+ formats: [],
24
+ replacements: [],
25
+ start: text.length,
26
+ end: text.length,
27
+ };
28
+ }
29
+
30
+ describe( 'useLastDifferentValue', () => {
31
+ it( 'should return the current record on first render', () => {
32
+ const record = makeRecord( 'Hello' );
33
+ const { result } = renderHook(
34
+ ( { value } ) => useLastDifferentValue( value ),
35
+ { initialProps: { value: record } }
36
+ );
37
+
38
+ expect( result.current.text ).toBe( 'Hello' );
39
+ } );
40
+
41
+ it( 'should return the previous record when text changes', () => {
42
+ const record1 = makeRecord( 'Hello' );
43
+ const { result, rerender } = renderHook(
44
+ ( { value } ) => useLastDifferentValue( value ),
45
+ { initialProps: { value: record1 } }
46
+ );
47
+
48
+ const record2 = makeRecord( 'Hello/' );
49
+ rerender( { value: record2 } );
50
+
51
+ expect( result.current.text ).toBe( 'Hello' );
52
+ } );
53
+
54
+ it( 'should not update when re-rendered with a new reference but same text', () => {
55
+ const record1 = makeRecord( 'Hello' );
56
+ const { result, rerender } = renderHook(
57
+ ( { value } ) => useLastDifferentValue( value ),
58
+ { initialProps: { value: record1 } }
59
+ );
60
+
61
+ // User types "/"
62
+ const record2 = makeRecord( 'Hello/' );
63
+ rerender( { value: record2 } );
64
+ expect( result.current.text ).toBe( 'Hello' );
65
+
66
+ // RESET_BLOCKS creates a new record object with the same text.
67
+ const record3 = makeRecord( 'Hello/' );
68
+ rerender( { value: record3 } );
69
+ expect( result.current.text ).toBe( 'Hello' );
70
+ } );
71
+
72
+ it( 'should survive multiple same-text re-renders', () => {
73
+ const record1 = makeRecord( 'Hello' );
74
+ const { result, rerender } = renderHook(
75
+ ( { value } ) => useLastDifferentValue( value ),
76
+ { initialProps: { value: record1 } }
77
+ );
78
+
79
+ // User types "/"
80
+ const record2 = makeRecord( 'Hello/' );
81
+ rerender( { value: record2 } );
82
+
83
+ // Multiple syncs, each producing new references with the same text.
84
+ for ( let i = 0; i < 5; i++ ) {
85
+ rerender( { value: makeRecord( 'Hello/' ) } );
86
+ }
87
+
88
+ expect( result.current.text ).toBe( 'Hello' );
89
+ } );
90
+
91
+ it( 'should track consecutive text changes correctly', () => {
92
+ const { result, rerender } = renderHook(
93
+ ( { value } ) => useLastDifferentValue( value ),
94
+ { initialProps: { value: makeRecord( 'A' ) } }
95
+ );
96
+
97
+ rerender( { value: makeRecord( 'AB' ) } );
98
+ expect( result.current.text ).toBe( 'A' );
99
+
100
+ rerender( { value: makeRecord( 'ABC' ) } );
101
+ expect( result.current.text ).toBe( 'AB' );
102
+
103
+ rerender( { value: makeRecord( 'ABCD' ) } );
104
+ expect( result.current.text ).toBe( 'ABC' );
105
+ } );
106
+
107
+ it( 'should update when cursor position changes without text change', () => {
108
+ const { result, rerender } = renderHook(
109
+ ( { value } ) => useLastDifferentValue( value ),
110
+ { initialProps: { value: makeRecord( 'Hello' ) } }
111
+ );
112
+
113
+ // User types "/"
114
+ rerender( { value: makeRecord( 'Hello/' ) } );
115
+ expect( result.current.text ).toBe( 'Hello' );
116
+
117
+ // User moves cursor left (same text, different position).
118
+ rerender( {
119
+ value: { ...makeRecord( 'Hello/' ), start: 0, end: 0 },
120
+ } );
121
+
122
+ // The returned record should now match the current text,
123
+ // so that didUserInput evaluates to false.
124
+ expect( result.current.text ).toBe( 'Hello/' );
125
+ } );
126
+ } );
127
+
19
128
  describe( 'AutocompleterUI', () => {
20
129
  describe( 'click outside behavior', () => {
21
130
  it( 'should call reset function when a click on another element occurs', async () => {
@@ -57,8 +166,6 @@ describe( 'AutocompleterUI', () => {
57
166
  },
58
167
  };
59
168
 
60
- const AutocompleterUI = getAutoCompleterUI( autocompleter );
61
-
62
169
  const OtherElement = <div>Other Element</div>;
63
170
 
64
171
  const Container = () => {
@@ -67,6 +174,7 @@ describe( 'AutocompleterUI', () => {
67
174
  return (
68
175
  <div>
69
176
  <AutocompleterUI
177
+ autocompleter={ autocompleter }
70
178
  className="test"
71
179
  filterValue="Apple"
72
180
  instanceId={ 1 }
@@ -109,6 +109,10 @@ export type WPCompleter< TCompleterOption = any > = {
109
109
  type ContentRef = React.RefObject< HTMLElement | null >;
110
110
 
111
111
  export type AutocompleterUIProps = {
112
+ /**
113
+ * The autocompleter configuration object.
114
+ */
115
+ autocompleter: WPCompleter;
112
116
  /**
113
117
  * The value to filter the options by.
114
118
  */
@@ -138,20 +142,10 @@ export type AutocompleterUIProps = {
138
142
  * A function to be called when an option is selected.
139
143
  */
140
144
  onSelect: ( option: KeyedOption ) => void;
141
- /**
142
- * A function to be called when the completer is reset
143
- * (e.g. when the user hits the escape key).
144
- */
145
- onReset?: () => void;
146
145
  /**
147
146
  * A function that defines the behavior of the completer when it is reset
148
147
  */
149
148
  reset: ( event: Event ) => void;
150
- // This is optional because it's still needed for mobile/native.
151
- /**
152
- * The rich text value object the autocompleter is being applied to.
153
- */
154
- value?: RichTextValue;
155
149
  /**
156
150
  * A ref containing the editable element that will serve as the anchor for
157
151
  * `Autocomplete`'s `Popover`.
@@ -192,6 +186,19 @@ export type UseAutocompleteProps = {
192
186
  contentRef: ContentRef;
193
187
  };
194
188
 
189
+ export type AutocompleteState = {
190
+ selectedIndex: number;
191
+ filteredOptions: KeyedOption[];
192
+ filterValue: string;
193
+ autocompleter: WPCompleter | null;
194
+ };
195
+
196
+ export type AutocompleteAction =
197
+ | { type: 'RESET' }
198
+ | { type: 'SELECT'; index: number }
199
+ | { type: 'OPTIONS'; options: KeyedOption[] }
200
+ | { type: 'MATCH'; completer: WPCompleter; query: string };
201
+
195
202
  export type AutocompleteProps = UseAutocompleteProps & {
196
203
  /**
197
204
  * A function that returns nodes to be rendered within the Autocomplete.
@@ -21,17 +21,12 @@ import {
21
21
  import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils';
22
22
  import {
23
23
  DEFAULT_VALUES,
24
- getInitialSide,
25
24
  isValueMixed,
26
25
  isValuesDefined,
27
26
  getAllowedSides,
28
27
  } from './utils';
29
28
  import { useControlledState } from '../utils/hooks';
30
- import type {
31
- BoxControlIconProps,
32
- BoxControlProps,
33
- BoxControlValue,
34
- } from './types';
29
+ import type { BoxControlProps, BoxControlValue } from './types';
35
30
  import { maybeWarnDeprecated36pxSize } from '../utils/deprecated-36px-size';
36
31
 
37
32
  const defaultInputProps = {
@@ -101,10 +96,6 @@ function BoxControl( {
101
96
  ! hasInitialValue || ! isValueMixed( inputValues ) || hasOneSide
102
97
  );
103
98
 
104
- const [ side, setSide ] = useState< BoxControlIconProps[ 'side' ] >(
105
- getInitialSide( isLinked, splitOnAxis )
106
- );
107
-
108
99
  // Tracking selected units via internal state allows filtering of CSS unit
109
100
  // only values from being saved while maintaining preexisting unit selection
110
101
  // behaviour. Filtering CSS only values prevents invalid style values.
@@ -120,14 +111,6 @@ function BoxControl( {
120
111
 
121
112
  const toggleLinked = () => {
122
113
  setIsLinked( ! isLinked );
123
- setSide( getInitialSide( ! isLinked, splitOnAxis ) );
124
- };
125
-
126
- const handleOnFocus = (
127
- _event: React.FocusEvent< HTMLInputElement >,
128
- { side: nextSide }: { side: typeof side }
129
- ) => {
130
- setSide( nextSide );
131
114
  };
132
115
 
133
116
  const handleOnChange = ( nextValues: BoxControlValue ) => {
@@ -148,7 +131,6 @@ function BoxControl( {
148
131
  onMouseOut,
149
132
  ...inputProps,
150
133
  onChange: handleOnChange,
151
- onFocus: handleOnFocus,
152
134
  isLinked,
153
135
  units,
154
136
  selectedUnits,
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
+ import deprecated from '@wordpress/deprecated';
4
5
  import { __ } from '@wordpress/i18n';
5
6
 
6
7
  /**
@@ -13,7 +14,6 @@ import type {
13
14
  CustomValueUnits,
14
15
  Preset,
15
16
  } from './types';
16
- import deprecated from '@wordpress/deprecated';
17
17
 
18
18
  export const CUSTOM_VALUE_SETTINGS: CustomValueUnits = {
19
19
  px: { max: 300, step: 1 },
@@ -160,24 +160,6 @@ export function isValuesDefined( values?: BoxControlValue ) {
160
160
  );
161
161
  }
162
162
 
163
- /**
164
- * Get initial selected side, factoring in whether the sides are linked,
165
- * and whether the vertical / horizontal directions are grouped via splitOnAxis.
166
- *
167
- * @param isLinked Whether the box control's fields are linked.
168
- * @param splitOnAxis Whether splitting by horizontal or vertical axis.
169
- * @return The initial side.
170
- */
171
- export function getInitialSide( isLinked: boolean, splitOnAxis: boolean ) {
172
- let initialSide: keyof typeof LABELS = 'all';
173
-
174
- if ( ! isLinked ) {
175
- initialSide = splitOnAxis ? 'vertical' : 'top';
176
- }
177
-
178
- return initialSide;
179
- }
180
-
181
163
  /**
182
164
  * Normalizes provided sides configuration to an array containing only top,
183
165
  * right, bottom and left. This essentially just maps `horizontal` or `vertical`
@@ -77,7 +77,7 @@ If provided, renders `a` instead of `button`.
77
77
 
78
78
  ### `icon`
79
79
 
80
- - Type: `IconType`
80
+ - Type: `IconType | null`
81
81
  - Required: No
82
82
 
83
83
  If provided, renders an Icon component inside the button.
@@ -18,7 +18,6 @@ import {
18
18
  /**
19
19
  * Internal dependencies
20
20
  */
21
- import './style.css';
22
21
  import Button from '..';
23
22
 
24
23
  const meta: Meta< typeof Button > = {
@@ -18,7 +18,7 @@
18
18
  font-weight: $font-weight-medium;
19
19
  margin: 0;
20
20
  border: 0;
21
- cursor: pointer;
21
+ cursor: var(--wpds-cursor-control);
22
22
  appearance: none;
23
23
  background: none;
24
24
 
@@ -410,12 +410,6 @@
410
410
  fill: CanvasText;
411
411
  }
412
412
  }
413
-
414
- // Fixes a Safari+VoiceOver bug, where the screen reader text is announced not respecting the source order.
415
- // See https://core.trac.wordpress.org/ticket/42006 and https://github.com/h5bp/html5-boilerplate/issues/1985
416
- .components-visually-hidden {
417
- height: auto;
418
- }
419
413
  }
420
414
 
421
415
  @keyframes components-button__busy-animation {
@@ -53,7 +53,7 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col
53
53
  background: none;
54
54
  padding: 0;
55
55
  margin: 0;
56
- cursor: pointer;
56
+ cursor: var(--wpds-cursor-control);
57
57
  justify-content: center;
58
58
  align-items: center;
59
59
  display: flex;
@@ -127,7 +127,7 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col
127
127
  background: none;
128
128
  padding: 0;
129
129
  margin: 0;
130
- cursor: pointer;
130
+ cursor: var(--wpds-cursor-control);
131
131
  appearance: none;
132
132
  display: inline-flex;
133
133
  align-items: center;
@@ -244,7 +244,7 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col
244
244
  }
245
245
 
246
246
  &:disabled::before {
247
- background-color: $components-color-disabled;
247
+ background-color: $components-color-gray-400;
248
248
  }
249
249
 
250
250
  &:hover:not(:disabled)::before {