@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
@@ -1,17 +1,12 @@
1
- /**
2
- * External dependencies
3
- */
4
- import removeAccents from 'remove-accents';
5
-
6
1
  /**
7
2
  * WordPress dependencies
8
3
  */
9
4
  import {
10
5
  renderToString,
11
6
  useEffect,
12
- useState,
13
- useRef,
14
7
  useMemo,
8
+ useReducer,
9
+ useRef,
15
10
  } from '@wordpress/element';
16
11
  import { useInstanceId, useMergeRefs, useRefEffect } from '@wordpress/compose';
17
12
  import {
@@ -27,18 +22,18 @@ import { isAppleOS } from '@wordpress/keycodes';
27
22
  /**
28
23
  * Internal dependencies
29
24
  */
30
- import { getAutoCompleterUI } from './autocompleter-ui';
31
- import { escapeRegExp } from '../utils/strings';
25
+ import { AutocompleterUI } from './autocompleter-ui';
26
+ import { getAutocompleteMatch } from './get-autocomplete-match';
32
27
  import { withIgnoreIMEEvents } from '../utils/with-ignore-ime-events';
33
28
  import type {
29
+ AutocompleteAction,
34
30
  AutocompleteProps,
35
- AutocompleterUIProps,
31
+ AutocompleteState,
36
32
  InsertOption,
37
33
  KeyedOption,
38
34
  OptionCompletion,
39
35
  ReplaceOption,
40
36
  UseAutocompleteProps,
41
- WPCompleter,
42
37
  } from './types';
43
38
  import getNodeText from '../utils/get-node-text';
44
39
 
@@ -47,6 +42,59 @@ const EMPTY_FILTERED_OPTIONS: KeyedOption[] = [];
47
42
  // Used for generating the instance ID
48
43
  const AUTOCOMPLETE_HOOK_REFERENCE = {};
49
44
 
45
+ function getCompletionObject(
46
+ completion: OptionCompletion
47
+ ): InsertOption | ReplaceOption {
48
+ if (
49
+ completion !== null &&
50
+ typeof completion === 'object' &&
51
+ 'action' in completion &&
52
+ completion.action !== undefined &&
53
+ 'value' in completion &&
54
+ completion.value !== undefined
55
+ ) {
56
+ return completion;
57
+ }
58
+ return {
59
+ action: 'insert-at-caret',
60
+ value: completion as React.ReactNode,
61
+ };
62
+ }
63
+
64
+ const initialState: AutocompleteState = {
65
+ selectedIndex: 0,
66
+ filteredOptions: EMPTY_FILTERED_OPTIONS,
67
+ filterValue: '',
68
+ autocompleter: null,
69
+ };
70
+
71
+ function autocompleteReducer(
72
+ state: AutocompleteState,
73
+ action: AutocompleteAction
74
+ ): AutocompleteState {
75
+ switch ( action.type ) {
76
+ case 'RESET':
77
+ return initialState;
78
+ case 'SELECT':
79
+ return { ...state, selectedIndex: action.index };
80
+ case 'OPTIONS':
81
+ return {
82
+ ...state,
83
+ filteredOptions: action.options,
84
+ selectedIndex:
85
+ action.options.length === state.filteredOptions.length
86
+ ? state.selectedIndex
87
+ : 0,
88
+ };
89
+ case 'MATCH':
90
+ return {
91
+ ...state,
92
+ autocompleter: action.completer,
93
+ filterValue: action.query,
94
+ };
95
+ }
96
+ }
97
+
50
98
  export function useAutocomplete( {
51
99
  record,
52
100
  onChange,
@@ -55,19 +103,9 @@ export function useAutocomplete( {
55
103
  contentRef,
56
104
  }: UseAutocompleteProps ) {
57
105
  const instanceId = useInstanceId( AUTOCOMPLETE_HOOK_REFERENCE );
58
- const [ selectedIndex, setSelectedIndex ] = useState( 0 );
59
-
60
- const [ filteredOptions, setFilteredOptions ] = useState<
61
- Array< KeyedOption >
62
- >( EMPTY_FILTERED_OPTIONS );
63
- const [ filterValue, setFilterValue ] =
64
- useState< AutocompleterUIProps[ 'filterValue' ] >( '' );
65
- const [ autocompleter, setAutocompleter ] = useState< WPCompleter | null >(
66
- null
67
- );
68
- const [ AutocompleterUI, setAutocompleterUI ] = useState<
69
- ( ( props: AutocompleterUIProps ) => React.JSX.Element | null ) | null
70
- >( null );
106
+ const [ state, dispatch ] = useReducer( autocompleteReducer, initialState );
107
+ const { selectedIndex, filteredOptions, filterValue, autocompleter } =
108
+ state;
71
109
 
72
110
  const backspacingRef = useRef( false );
73
111
 
@@ -91,27 +129,9 @@ export function useAutocomplete( {
91
129
  }
92
130
 
93
131
  if ( getOptionCompletion ) {
94
- const completion = getOptionCompletion( option.value, filterValue );
95
-
96
- const isCompletionObject = (
97
- obj: OptionCompletion
98
- ): obj is InsertOption | ReplaceOption => {
99
- return (
100
- obj !== null &&
101
- typeof obj === 'object' &&
102
- 'action' in obj &&
103
- obj.action !== undefined &&
104
- 'value' in obj &&
105
- obj.value !== undefined
106
- );
107
- };
108
-
109
- const completionObject = isCompletionObject( completion )
110
- ? completion
111
- : ( {
112
- action: 'insert-at-caret',
113
- value: completion,
114
- } as InsertOption );
132
+ const completionObject = getCompletionObject(
133
+ getOptionCompletion( option.value, filterValue )
134
+ );
115
135
 
116
136
  if ( 'replace' === completionObject.action ) {
117
137
  onReplace( [ completionObject.value ] );
@@ -125,31 +145,15 @@ export function useAutocomplete( {
125
145
 
126
146
  // Reset autocomplete state after insertion rather than before
127
147
  // so insertion events don't cause the completion menu to redisplay.
128
- reset();
148
+ dispatch( { type: 'RESET' } );
129
149
 
130
150
  // Make sure that the content remains focused after making a selection
131
151
  // and that the text cursor position is not lost.
132
152
  contentRef.current?.focus();
133
153
  }
134
154
 
135
- function reset() {
136
- setSelectedIndex( 0 );
137
- setFilteredOptions( EMPTY_FILTERED_OPTIONS );
138
- setFilterValue( '' );
139
- setAutocompleter( null );
140
- setAutocompleterUI( null );
141
- }
142
-
143
- /**
144
- * Load options for an autocompleter.
145
- *
146
- * @param {Array} options
147
- */
148
155
  function onChangeOptions( options: Array< KeyedOption > ) {
149
- setSelectedIndex(
150
- options.length === filteredOptions.length ? selectedIndex : 0
151
- );
152
- setFilteredOptions( options );
156
+ dispatch( { type: 'OPTIONS', options } );
153
157
  }
154
158
 
155
159
  function handleKeyDown( event: KeyboardEvent ) {
@@ -167,12 +171,13 @@ export function useAutocomplete( {
167
171
  }
168
172
 
169
173
  switch ( event.key ) {
170
- case 'ArrowUp': {
174
+ case 'ArrowUp':
175
+ case 'ArrowDown': {
176
+ const offset = event.key === 'ArrowUp' ? -1 : 1;
171
177
  const newIndex =
172
- ( selectedIndex === 0
173
- ? filteredOptions.length
174
- : selectedIndex ) - 1;
175
- setSelectedIndex( newIndex );
178
+ ( selectedIndex + offset + filteredOptions.length ) %
179
+ filteredOptions.length;
180
+ dispatch( { type: 'SELECT', index: newIndex } );
176
181
  // See the related PR as to why this is necessary: https://github.com/WordPress/gutenberg/pull/54902.
177
182
  if ( isAppleOS() ) {
178
183
  speak(
@@ -183,21 +188,8 @@ export function useAutocomplete( {
183
188
  break;
184
189
  }
185
190
 
186
- case 'ArrowDown': {
187
- const newIndex = ( selectedIndex + 1 ) % filteredOptions.length;
188
- setSelectedIndex( newIndex );
189
- if ( isAppleOS() ) {
190
- speak(
191
- getNodeText( filteredOptions[ newIndex ].label ),
192
- 'assertive'
193
- );
194
- }
195
- break;
196
- }
197
-
198
191
  case 'Escape':
199
- setAutocompleter( null );
200
- setAutocompleterUI( null );
192
+ dispatch( { type: 'RESET' } );
201
193
  event.preventDefault();
202
194
  break;
203
195
 
@@ -207,7 +199,7 @@ export function useAutocomplete( {
207
199
 
208
200
  case 'ArrowLeft':
209
201
  case 'ArrowRight':
210
- reset();
202
+ dispatch( { type: 'RESET' } );
211
203
  return;
212
204
 
213
205
  default:
@@ -230,132 +222,36 @@ export function useAutocomplete( {
230
222
  }, [ record ] );
231
223
 
232
224
  useEffect( () => {
233
- if ( ! textContent ) {
234
- if ( autocompleter ) {
235
- reset();
236
- }
237
- return;
238
- }
239
-
240
- // Find the completer with the highest triggerPrefix index in the
241
- // textContent.
242
- const completer = completers.reduce< WPCompleter | null >(
243
- ( lastTrigger, currentCompleter ) => {
244
- const triggerIndex = textContent.lastIndexOf(
245
- currentCompleter.triggerPrefix
246
- );
247
- const lastTriggerIndex =
248
- lastTrigger !== null
249
- ? textContent.lastIndexOf( lastTrigger.triggerPrefix )
250
- : -1;
251
-
252
- return triggerIndex > lastTriggerIndex
253
- ? currentCompleter
254
- : lastTrigger;
255
- },
256
- null
257
- );
258
-
259
- if ( ! completer ) {
260
- if ( autocompleter ) {
261
- reset();
262
- }
263
- return;
264
- }
265
-
266
- const { allowContext, triggerPrefix } = completer;
267
- const triggerIndex = textContent.lastIndexOf( triggerPrefix );
268
- const textWithoutTrigger = textContent.slice(
269
- triggerIndex + triggerPrefix.length
270
- );
271
-
272
- const tooDistantFromTrigger = textWithoutTrigger.length > 50; // 50 chars seems to be a good limit.
273
- // This is a final barrier to prevent the effect from completing with
274
- // an extremely long string, which causes the editor to slow-down
275
- // significantly. This could happen, for example, if `matchingWhileBackspacing`
276
- // is true and one of the "words" end up being too long. If that's the case,
277
- // it will be caught by this guard.
278
- if ( tooDistantFromTrigger ) {
279
- return;
280
- }
281
-
282
- const mismatch = filteredOptions.length === 0;
283
- const wordsFromTrigger = textWithoutTrigger.split( /\s/ );
284
- // We need to allow the effect to run when not backspacing and if there
285
- // was a mismatch. i.e when typing a trigger + the match string or when
286
- // clicking in an existing trigger word on the page. We do that if we
287
- // detect that we have one word from trigger in the current textual context.
288
- //
289
- // Ex.: "Some text @a" <-- "@a" will be detected as the trigger word and
290
- // allow the effect to run. It will run until there's a mismatch.
291
- const hasOneTriggerWord = wordsFromTrigger.length === 1;
292
- // This is used to allow the effect to run when backspacing and if
293
- // "touching" a word that "belongs" to a trigger. We consider a "trigger
294
- // word" any word up to the limit of 3 from the trigger character.
295
- // Anything beyond that is ignored if there's a mismatch. This allows
296
- // us to "escape" a mismatch when backspacing, but still imposing some
297
- // sane limits.
298
- //
299
- // Ex: "Some text @marcelo sekkkk" <--- "kkkk" caused a mismatch, but
300
- // if the user presses backspace here, it will show the completion popup again.
301
- const matchingWhileBackspacing =
302
- backspacingRef.current && wordsFromTrigger.length <= 3;
303
-
304
- if ( mismatch && ! ( matchingWhileBackspacing || hasOneTriggerWord ) ) {
305
- if ( autocompleter ) {
306
- reset();
307
- }
308
- return;
225
+ function getTextAfterSelection() {
226
+ return textContent
227
+ ? getTextContent(
228
+ slice(
229
+ record,
230
+ undefined,
231
+ getTextContent( record ).length
232
+ )
233
+ )
234
+ : '';
309
235
  }
310
236
 
311
- const textAfterSelection = getTextContent(
312
- slice( record, undefined, getTextContent( record ).length )
237
+ const match = getAutocompleteMatch(
238
+ textContent,
239
+ completers,
240
+ filteredOptions.length,
241
+ backspacingRef.current,
242
+ getTextAfterSelection
313
243
  );
314
244
 
315
- if (
316
- allowContext &&
317
- ! allowContext(
318
- textContent.slice( 0, triggerIndex ),
319
- textAfterSelection
320
- )
321
- ) {
245
+ if ( ! match ) {
322
246
  if ( autocompleter ) {
323
- reset();
247
+ dispatch( { type: 'RESET' } );
324
248
  }
325
249
  return;
326
250
  }
327
251
 
328
- if (
329
- /^\s/.test( textWithoutTrigger ) ||
330
- /\s\s+$/.test( textWithoutTrigger )
331
- ) {
332
- if ( autocompleter ) {
333
- reset();
334
- }
335
- return;
336
- }
252
+ const { completer, filterValue: query } = match;
337
253
 
338
- if ( ! /[\u0000-\uFFFF]*$/.test( textWithoutTrigger ) ) {
339
- if ( autocompleter ) {
340
- reset();
341
- }
342
- return;
343
- }
344
-
345
- const safeTrigger = escapeRegExp( completer.triggerPrefix );
346
- const text = removeAccents( textContent );
347
- const match = text
348
- .slice( text.lastIndexOf( completer.triggerPrefix ) )
349
- .match( new RegExp( `${ safeTrigger }([\u0000-\uFFFF]*)$` ) );
350
- const query = match && match[ 1 ];
351
-
352
- setAutocompleter( completer );
353
- setAutocompleterUI( () =>
354
- completer !== autocompleter
355
- ? getAutoCompleterUI( completer )
356
- : AutocompleterUI
357
- );
358
- setFilterValue( query === null ? '' : query );
254
+ dispatch( { type: 'MATCH', completer, query } );
359
255
  // We want to avoid introducing unexpected side effects.
360
256
  // See https://github.com/WordPress/gutenberg/pull/41820
361
257
  }, [ textContent ] );
@@ -370,7 +266,7 @@ export function useAutocomplete( {
370
266
  ? `components-autocomplete-item-${ instanceId }-${ selectedKey }`
371
267
  : null;
372
268
  const hasSelection = record.start !== undefined;
373
- const showPopover = !! textContent && hasSelection && !! AutocompleterUI;
269
+ const showPopover = !! textContent && hasSelection && !! autocompleter;
374
270
 
375
271
  return {
376
272
  listBoxId,
@@ -378,6 +274,8 @@ export function useAutocomplete( {
378
274
  onKeyDown: withIgnoreIMEEvents( handleKeyDown ),
379
275
  popover: showPopover && (
380
276
  <AutocompleterUI
277
+ key={ autocompleter.name + autocompleter.triggerPrefix }
278
+ autocompleter={ autocompleter }
381
279
  className={ className }
382
280
  filterValue={ filterValue }
383
281
  instanceId={ instanceId }
@@ -385,25 +283,48 @@ export function useAutocomplete( {
385
283
  selectedIndex={ selectedIndex }
386
284
  onChangeOptions={ onChangeOptions }
387
285
  onSelect={ select }
388
- value={ record }
389
286
  contentRef={ contentRef }
390
- reset={ reset }
287
+ reset={ () => dispatch( { type: 'RESET' } ) }
391
288
  />
392
289
  ),
393
290
  };
394
291
  }
395
292
 
396
- function useLastDifferentValue( value: UseAutocompleteProps[ 'record' ] ) {
397
- const history = useRef< Set< typeof value > >( new Set() );
293
+ /**
294
+ * Checks whether two records represent the same user-visible state
295
+ * (same text content and cursor position).
296
+ */
297
+ function recordValuesMatch(
298
+ a: UseAutocompleteProps[ 'record' ],
299
+ b: UseAutocompleteProps[ 'record' ]
300
+ ) {
301
+ return a.text === b.text && a.start === b.start && a.end === b.end;
302
+ }
398
303
 
399
- history.current.add( value );
304
+ /**
305
+ * Tracks the last record whose value differed from the current one.
306
+ * Used to determine whether the user has actually typed something
307
+ */
308
+ export function useLastDifferentValue(
309
+ value: UseAutocompleteProps[ 'record' ]
310
+ ) {
311
+ const history = useRef< Array< typeof value > >( [] );
312
+
313
+ const lastEntry = history.current[ history.current.length - 1 ];
314
+
315
+ // Only add to history if the value is meaningfully different from
316
+ // the most recent entry (analogous to Set.add being a no-op for
317
+ // duplicate references in the original implementation).
318
+ if ( ! lastEntry || ! recordValuesMatch( value, lastEntry ) ) {
319
+ history.current.push( value );
320
+ }
400
321
 
401
322
  // Keep the history size to 2.
402
- if ( history.current.size > 2 ) {
403
- history.current.delete( Array.from( history.current )[ 0 ] );
323
+ if ( history.current.length > 2 ) {
324
+ history.current.shift();
404
325
  }
405
326
 
406
- return Array.from( history.current )[ 0 ];
327
+ return history.current[ 0 ];
407
328
  }
408
329
 
409
330
  export function useAutocompleteProps( options: UseAutocompleteProps ) {