@wordpress/components 32.6.0 → 33.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 (208) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/build/autocomplete/get-autocomplete-match.cjs +11 -2
  3. package/build/autocomplete/get-autocomplete-match.cjs.map +2 -2
  4. package/build/autocomplete/index.cjs +42 -11
  5. package/build/autocomplete/index.cjs.map +2 -2
  6. package/build/external-link/index.cjs +1 -1
  7. package/build/external-link/index.cjs.map +2 -2
  8. package/build/form-token-field/index.cjs +22 -6
  9. package/build/form-token-field/index.cjs.map +3 -3
  10. package/build/form-token-field/token-input.cjs +1 -1
  11. package/build/form-token-field/token-input.cjs.map +2 -2
  12. package/build/menu/popover.cjs +7 -3
  13. package/build/menu/popover.cjs.map +2 -2
  14. package/build/menu/styles.cjs +39 -16
  15. package/build/menu/styles.cjs.map +2 -2
  16. package/build/navigable-container/container.cjs +72 -110
  17. package/build/navigable-container/container.cjs.map +2 -2
  18. package/build/utils/breakpoint.cjs.map +1 -1
  19. package/build/utils/font.cjs.map +1 -1
  20. package/build/visually-hidden/component.cjs +1 -0
  21. package/build/visually-hidden/component.cjs.map +2 -2
  22. package/build-module/autocomplete/get-autocomplete-match.mjs +11 -2
  23. package/build-module/autocomplete/get-autocomplete-match.mjs.map +2 -2
  24. package/build-module/autocomplete/index.mjs +42 -11
  25. package/build-module/autocomplete/index.mjs.map +2 -2
  26. package/build-module/external-link/index.mjs +1 -1
  27. package/build-module/external-link/index.mjs.map +2 -2
  28. package/build-module/form-token-field/index.mjs +22 -6
  29. package/build-module/form-token-field/index.mjs.map +2 -2
  30. package/build-module/form-token-field/token-input.mjs +1 -1
  31. package/build-module/form-token-field/token-input.mjs.map +2 -2
  32. package/build-module/menu/popover.mjs +7 -3
  33. package/build-module/menu/popover.mjs.map +2 -2
  34. package/build-module/menu/styles.mjs +37 -16
  35. package/build-module/menu/styles.mjs.map +2 -2
  36. package/build-module/navigable-container/container.mjs +73 -111
  37. package/build-module/navigable-container/container.mjs.map +2 -2
  38. package/build-module/utils/breakpoint.mjs.map +1 -1
  39. package/build-module/utils/font.mjs.map +1 -1
  40. package/build-module/visually-hidden/component.mjs +1 -0
  41. package/build-module/visually-hidden/component.mjs.map +2 -2
  42. package/build-style/style-rtl.css +26 -2
  43. package/build-style/style.css +26 -2
  44. package/build-types/autocomplete/get-autocomplete-match.d.ts +10 -1
  45. package/build-types/autocomplete/get-autocomplete-match.d.ts.map +1 -1
  46. package/build-types/autocomplete/index.d.ts.map +1 -1
  47. package/build-types/base-control/stories/index.story.d.ts.map +1 -1
  48. package/build-types/button/stories/index.story.d.ts.map +1 -1
  49. package/build-types/card/stories/index.story.d.ts +0 -6
  50. package/build-types/card/stories/index.story.d.ts.map +1 -1
  51. package/build-types/checkbox-control/stories/index.story.d.ts.map +1 -1
  52. package/build-types/color-indicator/stories/index.story.d.ts.map +1 -1
  53. package/build-types/color-palette/stories/index.story.d.ts.map +1 -1
  54. package/build-types/color-picker/stories/index.story.d.ts.map +1 -1
  55. package/build-types/combobox-control/stories/index.story.d.ts.map +1 -1
  56. package/build-types/composite/stories/index.story.d.ts.map +1 -1
  57. package/build-types/custom-select-control/stories/index.story.d.ts.map +1 -1
  58. package/build-types/disabled/stories/index.story.d.ts.map +1 -1
  59. package/build-types/drop-zone/stories/index.story.d.ts.map +1 -1
  60. package/build-types/dropdown/stories/index.story.d.ts.map +1 -1
  61. package/build-types/external-link/index.d.ts.map +1 -1
  62. package/build-types/external-link/stories/index.story.d.ts.map +1 -1
  63. package/build-types/form-file-upload/stories/index.story.d.ts.map +1 -1
  64. package/build-types/form-toggle/stories/index.story.d.ts.map +1 -1
  65. package/build-types/form-token-field/index.d.ts.map +1 -1
  66. package/build-types/form-token-field/stories/index.story.d.ts.map +1 -1
  67. package/build-types/form-token-field/token-input.d.ts.map +1 -1
  68. package/build-types/form-token-field/types.d.ts +16 -2
  69. package/build-types/form-token-field/types.d.ts.map +1 -1
  70. package/build-types/gradient-picker/stories/index.story.d.ts.map +1 -1
  71. package/build-types/icon/stories/index.story.d.ts.map +1 -1
  72. package/build-types/keyboard-shortcuts/stories/index.story.d.ts.map +1 -1
  73. package/build-types/menu/popover.d.ts.map +1 -1
  74. package/build-types/menu/styles.d.ts +16 -1
  75. package/build-types/menu/styles.d.ts.map +1 -1
  76. package/build-types/menu-group/stories/index.story.d.ts.map +1 -1
  77. package/build-types/menu-item/stories/index.story.d.ts.map +1 -1
  78. package/build-types/menu-items-choice/stories/index.story.d.ts.map +1 -1
  79. package/build-types/modal/stories/index.story.d.ts.map +1 -1
  80. package/build-types/navigable-container/container.d.ts +3 -8
  81. package/build-types/navigable-container/container.d.ts.map +1 -1
  82. package/build-types/navigable-container/types.d.ts +1 -5
  83. package/build-types/navigable-container/types.d.ts.map +1 -1
  84. package/build-types/navigation/stories/utils/more-examples.d.ts.map +1 -1
  85. package/build-types/navigator/stories/index.story.d.ts.map +1 -1
  86. package/build-types/notice/stories/index.story.d.ts.map +1 -1
  87. package/build-types/panel/stories/index.story.d.ts.map +1 -1
  88. package/build-types/popover/stories/index.story.d.ts.map +1 -1
  89. package/build-types/progress-bar/stories/index.story.d.ts.map +1 -1
  90. package/build-types/radio-control/stories/index.story.d.ts.map +1 -1
  91. package/build-types/range-control/stories/index.story.d.ts.map +1 -1
  92. package/build-types/resizable-box/stories/index.story.d.ts.map +1 -1
  93. package/build-types/sandbox/stories/index.story.d.ts.map +1 -1
  94. package/build-types/scroll-lock/stories/index.story.d.ts.map +1 -1
  95. package/build-types/search-control/stories/index.story.d.ts.map +1 -1
  96. package/build-types/select-control/stories/index.story.d.ts.map +1 -1
  97. package/build-types/shortcut/stories/index.story.d.ts.map +1 -1
  98. package/build-types/slot-fill/stories/index.story.d.ts.map +1 -1
  99. package/build-types/snackbar/stories/index.story.d.ts.map +1 -1
  100. package/build-types/spinner/stories/index.story.d.ts.map +1 -1
  101. package/build-types/text-control/stories/index.story.d.ts.map +1 -1
  102. package/build-types/text-highlight/stories/index.story.d.ts.map +1 -1
  103. package/build-types/textarea-control/stories/index.story.d.ts.map +1 -1
  104. package/build-types/toggle-control/stories/index.story.d.ts.map +1 -1
  105. package/build-types/tooltip/stories/index.story.d.ts.map +1 -1
  106. package/build-types/tree-select/stories/index.story.d.ts.map +1 -1
  107. package/build-types/utils/breakpoint.d.ts +2 -1
  108. package/build-types/utils/breakpoint.d.ts.map +1 -1
  109. package/build-types/utils/font.d.ts +3 -2
  110. package/build-types/utils/font.d.ts.map +1 -1
  111. package/build-types/visually-hidden/component.d.ts.map +1 -1
  112. package/build-types/visually-hidden/stories/index.story.d.ts +0 -6
  113. package/build-types/visually-hidden/stories/index.story.d.ts.map +1 -1
  114. package/package.json +21 -21
  115. package/src/autocomplete/get-autocomplete-match.ts +25 -4
  116. package/src/autocomplete/index.tsx +69 -21
  117. package/src/autocomplete/test/get-autocomplete-match.ts +97 -75
  118. package/src/base-control/stories/index.story.tsx +1 -0
  119. package/src/button/stories/index.story.tsx +1 -0
  120. package/src/button-group/style.scss +1 -2
  121. package/src/card/stories/index.story.tsx +2 -9
  122. package/src/checkbox-control/stories/index.story.tsx +1 -0
  123. package/src/circular-option-picker/style.scss +8 -6
  124. package/src/color-indicator/stories/index.story.tsx +1 -0
  125. package/src/color-palette/stories/index.story.tsx +1 -0
  126. package/src/color-picker/stories/index.story.tsx +1 -0
  127. package/src/combobox-control/stories/index.story.tsx +1 -0
  128. package/src/composite/stories/index.story.tsx +1 -0
  129. package/src/confirm-dialog/stories/index.story.tsx +1 -1
  130. package/src/custom-select-control/stories/index.story.tsx +1 -0
  131. package/src/disabled/stories/index.story.tsx +1 -0
  132. package/src/drop-zone/stories/index.story.tsx +1 -0
  133. package/src/dropdown/stories/index.story.tsx +1 -0
  134. package/src/external-link/index.tsx +1 -6
  135. package/src/external-link/stories/index.story.tsx +2 -1
  136. package/src/external-link/style.scss +30 -2
  137. package/src/form-file-upload/stories/index.story.tsx +1 -0
  138. package/src/form-toggle/stories/index.story.tsx +1 -0
  139. package/src/form-toggle/style.scss +3 -2
  140. package/src/form-token-field/README.md +2 -1
  141. package/src/form-token-field/index.tsx +39 -9
  142. package/src/form-token-field/stories/index.story.tsx +2 -0
  143. package/src/form-token-field/test/index.tsx +70 -10
  144. package/src/form-token-field/token-input.tsx +1 -6
  145. package/src/form-token-field/types.ts +16 -2
  146. package/src/gradient-picker/stories/index.story.tsx +1 -0
  147. package/src/icon/stories/index.story.tsx +1 -0
  148. package/src/input-control/stories/index.story.tsx +1 -1
  149. package/src/item-group/stories/index.story.tsx +1 -1
  150. package/src/keyboard-shortcuts/stories/index.story.tsx +1 -0
  151. package/src/menu/popover.tsx +15 -8
  152. package/src/menu/styles.ts +26 -16
  153. package/src/menu/test/index.tsx +24 -34
  154. package/src/menu-group/stories/index.story.tsx +1 -0
  155. package/src/menu-item/stories/index.story.tsx +1 -0
  156. package/src/menu-items-choice/stories/index.story.tsx +1 -0
  157. package/src/mobile/link-settings/index.native.js +1 -1
  158. package/src/modal/stories/index.story.tsx +1 -0
  159. package/src/navigable-container/container.tsx +120 -141
  160. package/src/navigable-container/test/navigable-menu.tsx +24 -0
  161. package/src/navigable-container/types.ts +1 -5
  162. package/src/navigation/stories/utils/more-examples.tsx +2 -1
  163. package/src/navigator/stories/index.story.tsx +1 -0
  164. package/src/notice/stories/index.story.tsx +1 -0
  165. package/src/notice/test/__snapshots__/index.tsx.snap +1 -0
  166. package/src/number-control/stories/index.story.tsx +1 -1
  167. package/src/panel/stories/index.story.tsx +1 -0
  168. package/src/popover/stories/index.story.tsx +1 -0
  169. package/src/progress-bar/stories/index.story.tsx +1 -0
  170. package/src/radio-control/stories/index.story.tsx +1 -0
  171. package/src/range-control/stories/index.story.tsx +1 -0
  172. package/src/resizable-box/stories/index.story.tsx +1 -0
  173. package/src/resizable-box/style.scss +4 -5
  174. package/src/sandbox/stories/index.story.tsx +1 -0
  175. package/src/scroll-lock/stories/index.story.tsx +1 -0
  176. package/src/search-control/stories/index.story.tsx +1 -0
  177. package/src/select-control/stories/index.story.tsx +1 -0
  178. package/src/shortcut/stories/index.story.tsx +1 -0
  179. package/src/slot-fill/stories/index.story.tsx +1 -0
  180. package/src/snackbar/stories/index.story.tsx +1 -0
  181. package/src/spinner/stories/index.story.tsx +1 -0
  182. package/src/text-control/stories/index.story.tsx +1 -0
  183. package/src/text-highlight/stories/index.story.tsx +1 -0
  184. package/src/textarea-control/stories/index.story.tsx +1 -0
  185. package/src/toggle-control/stories/index.story.tsx +1 -0
  186. package/src/toggle-group-control/stories/index.story.tsx +1 -1
  187. package/src/toolbar/toolbar-group/index.tsx +2 -2
  188. package/src/tooltip/stories/index.story.tsx +1 -0
  189. package/src/tooltip/test/index.tsx +3 -2
  190. package/src/tree-grid/stories/index.story.tsx +1 -1
  191. package/src/tree-select/stories/index.story.tsx +1 -0
  192. package/src/truncate/stories/index.story.tsx +1 -1
  193. package/src/unit-control/stories/index.story.tsx +1 -1
  194. package/src/utils/breakpoint.js +1 -1
  195. package/src/utils/font.js +1 -1
  196. package/src/visually-hidden/component.tsx +1 -0
  197. package/src/visually-hidden/stories/index.story.tsx +2 -8
  198. package/build/card/context.cjs +0 -36
  199. package/build/card/context.cjs.map +0 -7
  200. package/build-module/card/context.mjs +0 -10
  201. package/build-module/card/context.mjs.map +0 -7
  202. package/build-types/card/context.d.ts +0 -3
  203. package/build-types/card/context.d.ts.map +0 -1
  204. package/build-types/visually-hidden/test/index.d.ts +0 -2
  205. package/build-types/visually-hidden/test/index.d.ts.map +0 -1
  206. package/src/card/context.ts +0 -9
  207. package/src/visually-hidden/test/__snapshots__/index.tsx.snap +0 -12
  208. package/src/visually-hidden/test/index.tsx +0 -17
@@ -108,10 +108,15 @@ export function useAutocomplete( {
108
108
  state;
109
109
 
110
110
  const backspacingRef = useRef( false );
111
+ const prevRecordTextRef = useRef( '' );
112
+ const lastCompletionRef = useRef< {
113
+ name: string;
114
+ value: string;
115
+ } | null >( null );
111
116
 
112
117
  function insertCompletion( replacement: React.ReactNode ) {
113
118
  if ( autocompleter === null ) {
114
- return;
119
+ return '';
115
120
  }
116
121
  const end = record.start;
117
122
  const start =
@@ -119,27 +124,48 @@ export function useAutocomplete( {
119
124
  const toInsert = create( { html: renderToString( replacement ) } );
120
125
 
121
126
  onChange( insert( record, toInsert, start, end ) );
127
+ return getTextContent( toInsert );
122
128
  }
123
129
 
124
130
  function select( option: KeyedOption ) {
125
- const { getOptionCompletion } = autocompleter || {};
131
+ if ( option.isDisabled || ! autocompleter ) {
132
+ return;
133
+ }
126
134
 
127
- if ( option.isDisabled ) {
135
+ const { getOptionCompletion } = autocompleter;
136
+ if ( ! getOptionCompletion ) {
137
+ dispatch( { type: 'RESET' } );
138
+ contentRef.current?.focus();
128
139
  return;
129
140
  }
130
141
 
131
- if ( getOptionCompletion ) {
132
- const completionObject = getCompletionObject(
133
- getOptionCompletion( option.value, filterValue )
134
- );
142
+ const completionObject = getCompletionObject(
143
+ getOptionCompletion( option.value, filterValue )
144
+ );
145
+
146
+ if ( 'replace' === completionObject.action ) {
147
+ onReplace( [ completionObject.value ] );
148
+ // When replacing, the component will unmount, so don't reset
149
+ // state (below) on an unmounted component.
150
+ return;
151
+ }
135
152
 
136
- if ( 'replace' === completionObject.action ) {
137
- onReplace( [ completionObject.value ] );
138
- // When replacing, the component will unmount, so don't reset
139
- // state (below) on an unmounted component.
140
- return;
141
- } else if ( 'insert-at-caret' === completionObject.action ) {
142
- insertCompletion( completionObject.value );
153
+ if ( 'insert-at-caret' === completionObject.action ) {
154
+ const completionText = insertCompletion( completionObject.value );
155
+ // When the completion value starts with the trigger prefix
156
+ // (e.g. @username), the trigger stays in the text and would
157
+ // re-activate the autocompleter. Store the completed text so
158
+ // the effect can suppress the stale re-match.
159
+ if ( completionText.startsWith( autocompleter.triggerPrefix ) ) {
160
+ const afterPrefix = completionText.slice(
161
+ autocompleter.triggerPrefix.length
162
+ );
163
+ if ( afterPrefix ) {
164
+ lastCompletionRef.current = {
165
+ name: autocompleter.name,
166
+ value: afterPrefix,
167
+ };
168
+ }
143
169
  }
144
170
  }
145
171
 
@@ -222,6 +248,9 @@ export function useAutocomplete( {
222
248
  }, [ record ] );
223
249
 
224
250
  useEffect( () => {
251
+ const isTextChange = record.text !== prevRecordTextRef.current;
252
+ prevRecordTextRef.current = record.text;
253
+
225
254
  function getTextAfterSelection() {
226
255
  return textContent
227
256
  ? getTextContent(
@@ -234,13 +263,12 @@ export function useAutocomplete( {
234
263
  : '';
235
264
  }
236
265
 
237
- const match = getAutocompleteMatch(
238
- textContent,
239
- completers,
240
- filteredOptions.length,
241
- backspacingRef.current,
242
- getTextAfterSelection
243
- );
266
+ const match = getAutocompleteMatch( textContent, completers, {
267
+ matchCount: filteredOptions.length,
268
+ isBackspacing: backspacingRef.current,
269
+ getTextAfterSelection,
270
+ lastCompletion: lastCompletionRef.current,
271
+ } );
244
272
 
245
273
  if ( ! match ) {
246
274
  if ( autocompleter ) {
@@ -251,6 +279,26 @@ export function useAutocomplete( {
251
279
 
252
280
  const { completer, filterValue: query } = match;
253
281
 
282
+ // Don't re-activate a dismissed autocompleter on cursor-only
283
+ // movement. `textContent` (text before cursor) changes with the
284
+ // caret, so the effect re-runs, but `record.text` does not.
285
+ // Complements the render-time `didUserInput` gate in
286
+ // `useAutocompleteProps` for callers using this hook directly.
287
+ if ( ! autocompleter && ! isTextChange ) {
288
+ return;
289
+ }
290
+
291
+ // Clear stale completion ref when the user types a new trigger
292
+ // for the same completer (the previous completion is no longer
293
+ // relevant). Must be after the cursor-only check so that mere
294
+ // cursor movement doesn't discard the suppression state.
295
+ if (
296
+ lastCompletionRef.current &&
297
+ lastCompletionRef.current.name === completer.name
298
+ ) {
299
+ lastCompletionRef.current = null;
300
+ }
301
+
254
302
  dispatch( { type: 'MATCH', completer, query } );
255
303
  // We want to avoid introducing unexpected side effects.
256
304
  // See https://github.com/WordPress/gutenberg/pull/41820
@@ -14,17 +14,23 @@ const createCompleter = (
14
14
  ...overrides,
15
15
  } );
16
16
 
17
+ const defaultOptions = {
18
+ matchCount: 1,
19
+ isBackspacing: false,
20
+ getTextAfterSelection: () => '',
21
+ };
22
+
17
23
  describe( 'getAutocompleteMatch', () => {
18
24
  it( 'should return null for empty text content', () => {
19
25
  const completers = [ createCompleter() ];
20
26
  expect(
21
- getAutocompleteMatch( '', completers, 0, false, () => '' )
27
+ getAutocompleteMatch( '', completers, defaultOptions )
22
28
  ).toBeNull();
23
29
  } );
24
30
 
25
31
  it( 'should return null when no completers are provided', () => {
26
32
  expect(
27
- getAutocompleteMatch( 'some text /', [], 0, false, () => '' )
33
+ getAutocompleteMatch( 'some text /', [], defaultOptions )
28
34
  ).toBeNull();
29
35
  } );
30
36
 
@@ -34,9 +40,7 @@ describe( 'getAutocompleteMatch', () => {
34
40
  getAutocompleteMatch(
35
41
  'no trigger here',
36
42
  completers,
37
- 1,
38
- false,
39
- () => ''
43
+ defaultOptions
40
44
  )
41
45
  ).toBeNull();
42
46
  } );
@@ -46,9 +50,7 @@ describe( 'getAutocompleteMatch', () => {
46
50
  const result = getAutocompleteMatch(
47
51
  'some text /query',
48
52
  completers,
49
- 1,
50
- false,
51
- () => ''
53
+ defaultOptions
52
54
  );
53
55
  expect( result ).toEqual( {
54
56
  completer: completers[ 0 ],
@@ -61,9 +63,7 @@ describe( 'getAutocompleteMatch', () => {
61
63
  const result = getAutocompleteMatch(
62
64
  'hello @',
63
65
  completers,
64
- 1,
65
- false,
66
- () => ''
66
+ defaultOptions
67
67
  );
68
68
  expect( result ).toEqual( {
69
69
  completer: completers[ 0 ],
@@ -83,9 +83,7 @@ describe( 'getAutocompleteMatch', () => {
83
83
  const result = getAutocompleteMatch(
84
84
  '/command some text @user',
85
85
  [ slashCompleter, atCompleter ],
86
- 1,
87
- false,
88
- () => ''
86
+ defaultOptions
89
87
  );
90
88
  expect( result?.completer.name ).toBe( 'at' );
91
89
  } );
@@ -94,47 +92,35 @@ describe( 'getAutocompleteMatch', () => {
94
92
  const completers = [ createCompleter( { triggerPrefix: '/' } ) ];
95
93
  const longText = '/' + 'a'.repeat( 51 );
96
94
  expect(
97
- getAutocompleteMatch( longText, completers, 1, false, () => '' )
95
+ getAutocompleteMatch( longText, completers, defaultOptions )
98
96
  ).toBeNull();
99
97
  } );
100
98
 
101
99
  it( 'should match when text after trigger is exactly 50 chars', () => {
102
100
  const completers = [ createCompleter( { triggerPrefix: '/' } ) ];
103
101
  const text = '/' + 'a'.repeat( 50 );
104
- const result = getAutocompleteMatch(
105
- text,
106
- completers,
107
- 1,
108
- false,
109
- () => ''
110
- );
102
+ const result = getAutocompleteMatch( text, completers, defaultOptions );
111
103
  expect( result ).not.toBeNull();
112
104
  expect( result?.filterValue ).toBe( 'a'.repeat( 50 ) );
113
105
  } );
114
106
 
115
107
  it( 'should return null on mismatch with multiple words and no backspacing', () => {
116
108
  const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
117
- // 4 words from trigger, mismatch (filteredOptionsLength=0), not backspacing
109
+ // 4 words from trigger, mismatch (matchCount=0), not backspacing
118
110
  expect(
119
- getAutocompleteMatch(
120
- 'text @one two three four',
121
- completers,
122
- 0,
123
- false,
124
- () => ''
125
- )
111
+ getAutocompleteMatch( 'text @one two three four', completers, {
112
+ ...defaultOptions,
113
+ matchCount: 0,
114
+ } )
126
115
  ).toBeNull();
127
116
  } );
128
117
 
129
118
  it( 'should still match on mismatch when there is only one trigger word', () => {
130
119
  const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
131
- const result = getAutocompleteMatch(
132
- 'text @xyz',
133
- completers,
134
- 0,
135
- false,
136
- () => ''
137
- );
120
+ const result = getAutocompleteMatch( 'text @xyz', completers, {
121
+ ...defaultOptions,
122
+ matchCount: 0,
123
+ } );
138
124
  expect( result ).not.toBeNull();
139
125
  expect( result?.filterValue ).toBe( 'xyz' );
140
126
  } );
@@ -144,9 +130,11 @@ describe( 'getAutocompleteMatch', () => {
144
130
  const result = getAutocompleteMatch(
145
131
  'text @one two three',
146
132
  completers,
147
- 0,
148
- true,
149
- () => ''
133
+ {
134
+ ...defaultOptions,
135
+ matchCount: 0,
136
+ isBackspacing: true,
137
+ }
150
138
  );
151
139
  expect( result ).not.toBeNull();
152
140
  } );
@@ -154,27 +142,25 @@ describe( 'getAutocompleteMatch', () => {
154
142
  it( 'should NOT match while backspacing if more than 3 words from trigger', () => {
155
143
  const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
156
144
  expect(
157
- getAutocompleteMatch(
158
- 'text @one two three four',
159
- completers,
160
- 0,
161
- true,
162
- () => ''
163
- )
145
+ getAutocompleteMatch( 'text @one two three four', completers, {
146
+ ...defaultOptions,
147
+ matchCount: 0,
148
+ isBackspacing: true,
149
+ } )
164
150
  ).toBeNull();
165
151
  } );
166
152
 
167
153
  it( 'should return null when text after trigger starts with whitespace', () => {
168
154
  const completers = [ createCompleter( { triggerPrefix: '/' } ) ];
169
155
  expect(
170
- getAutocompleteMatch( '/ query', completers, 1, false, () => '' )
156
+ getAutocompleteMatch( '/ query', completers, defaultOptions )
171
157
  ).toBeNull();
172
158
  } );
173
159
 
174
160
  it( 'should return null when text after trigger ends with multiple spaces', () => {
175
161
  const completers = [ createCompleter( { triggerPrefix: '/' } ) ];
176
162
  expect(
177
- getAutocompleteMatch( '/query ', completers, 1, false, () => '' )
163
+ getAutocompleteMatch( '/query ', completers, defaultOptions )
178
164
  ).toBeNull();
179
165
  } );
180
166
 
@@ -186,7 +172,7 @@ describe( 'getAutocompleteMatch', () => {
186
172
  } ),
187
173
  ];
188
174
  expect(
189
- getAutocompleteMatch( 'text @user', completers, 1, false, () => '' )
175
+ getAutocompleteMatch( 'text @user', completers, defaultOptions )
190
176
  ).toBeNull();
191
177
  } );
192
178
 
@@ -198,13 +184,10 @@ describe( 'getAutocompleteMatch', () => {
198
184
  allowContext,
199
185
  } ),
200
186
  ];
201
- getAutocompleteMatch(
202
- 'before @user',
203
- completers,
204
- 1,
205
- false,
206
- () => 'after'
207
- );
187
+ getAutocompleteMatch( 'before @user', completers, {
188
+ ...defaultOptions,
189
+ getTextAfterSelection: () => 'after',
190
+ } );
208
191
  expect( allowContext ).toHaveBeenCalledWith( 'before ', 'after' );
209
192
  } );
210
193
 
@@ -213,9 +196,7 @@ describe( 'getAutocompleteMatch', () => {
213
196
  const result = getAutocompleteMatch(
214
197
  'text @café',
215
198
  completers,
216
- 1,
217
- false,
218
- () => ''
199
+ defaultOptions
219
200
  );
220
201
  expect( result ).not.toBeNull();
221
202
  expect( result?.filterValue ).toBe( 'cafe' );
@@ -233,9 +214,7 @@ describe( 'getAutocompleteMatch', () => {
233
214
  const result = getAutocompleteMatch(
234
215
  '@@user',
235
216
  [ singleAt, doubleAt ],
236
- 1,
237
- false,
238
- () => ''
217
+ defaultOptions
239
218
  );
240
219
  expect( result?.completer.name ).toBe( 'double' );
241
220
  expect( result?.filterValue ).toBe( 'user' );
@@ -253,9 +232,7 @@ describe( 'getAutocompleteMatch', () => {
253
232
  const result = getAutocompleteMatch(
254
233
  'hello @user',
255
234
  [ singleAt, doubleAt ],
256
- 1,
257
- false,
258
- () => ''
235
+ defaultOptions
259
236
  );
260
237
  expect( result?.completer.name ).toBe( 'single' );
261
238
  expect( result?.filterValue ).toBe( 'user' );
@@ -266,9 +243,7 @@ describe( 'getAutocompleteMatch', () => {
266
243
  const result = getAutocompleteMatch(
267
244
  'text $$query',
268
245
  completers,
269
- 1,
270
- false,
271
- () => ''
246
+ defaultOptions
272
247
  );
273
248
  expect( result ).not.toBeNull();
274
249
  expect( result?.filterValue ).toBe( 'query' );
@@ -279,14 +254,63 @@ describe( 'getAutocompleteMatch', () => {
279
254
  const result = getAutocompleteMatch(
280
255
  '/hello world',
281
256
  completers,
282
- 1,
283
- false,
284
- () => ''
257
+ defaultOptions
285
258
  );
286
259
  expect( result ).not.toBeNull();
287
260
  expect( result?.filterValue ).toBe( 'hello world' );
288
261
  } );
289
262
 
263
+ describe( 'lastCompletion suppression', () => {
264
+ it( 'should suppress match when text equals last completion value', () => {
265
+ const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
266
+ expect(
267
+ getAutocompleteMatch( '@user', completers, {
268
+ ...defaultOptions,
269
+ lastCompletion: {
270
+ name: completers[ 0 ].name,
271
+ value: 'user',
272
+ },
273
+ } )
274
+ ).toBeNull();
275
+ } );
276
+
277
+ it( 'should suppress match ignoring trailing whitespace', () => {
278
+ const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
279
+ expect(
280
+ getAutocompleteMatch( '@user ', completers, {
281
+ ...defaultOptions,
282
+ lastCompletion: {
283
+ name: completers[ 0 ].name,
284
+ value: 'user',
285
+ },
286
+ } )
287
+ ).toBeNull();
288
+ } );
289
+
290
+ it( 'should NOT suppress match when completer name differs', () => {
291
+ const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
292
+ const result = getAutocompleteMatch( '@user', completers, {
293
+ ...defaultOptions,
294
+ lastCompletion: { name: 'other', value: 'user' },
295
+ } );
296
+ expect( result ).not.toBeNull();
297
+ expect( result?.filterValue ).toBe( 'user' );
298
+ } );
299
+
300
+ it( 'should NOT suppress match when text diverges from last completion', () => {
301
+ const completers = [ createCompleter( { triggerPrefix: '@' } ) ];
302
+ const result = getAutocompleteMatch( '@user2', completers, {
303
+ ...defaultOptions,
304
+ lastCompletion: {
305
+ name: completers[ 0 ].name,
306
+ value: 'user',
307
+ },
308
+ } );
309
+ expect( result ).not.toBeNull();
310
+ expect( result?.filterValue ).toBe( 'user2' );
311
+ } );
312
+ } );
313
+
290
314
  it.each( [
291
315
  {
292
316
  text: 'café @user',
@@ -327,9 +351,7 @@ describe( 'getAutocompleteMatch', () => {
327
351
  const result = getAutocompleteMatch(
328
352
  text,
329
353
  completers,
330
- 1,
331
- false,
332
- () => ''
354
+ defaultOptions
333
355
  );
334
356
  expect( result ).not.toBeNull();
335
357
  expect( result?.filterValue ).toBe( expected );
@@ -10,6 +10,7 @@ import BaseControl, { useBaseControlProps } from '..';
10
10
  import Button from '../../button';
11
11
 
12
12
  const meta: Meta< typeof BaseControl > = {
13
+ tags: [ 'manifest' ],
13
14
  title: 'Components/Selection & Input/Common/BaseControl',
14
15
  id: 'components-basecontrol',
15
16
  component: BaseControl,
@@ -21,6 +21,7 @@ import {
21
21
  import Button from '..';
22
22
 
23
23
  const meta: Meta< typeof Button > = {
24
+ tags: [ 'manifest' ],
24
25
  title: 'Components/Actions/Button',
25
26
  id: 'components-button',
26
27
  component: Button,
@@ -1,6 +1,5 @@
1
1
  @use "@wordpress/base-styles/colors" as *;
2
2
  @use "@wordpress/base-styles/variables" as *;
3
- @use "@wordpress/base-styles/z-index" as *;
4
3
 
5
4
  .components-button-group {
6
5
  display: inline-block;
@@ -28,7 +27,7 @@
28
27
  &:focus,
29
28
  &.is-primary {
30
29
  position: relative;
31
- z-index: z-index(".components-button {:focus or .is-primary}");
30
+ z-index: 1;
32
31
  }
33
32
 
34
33
  // The active button should look pressed.
@@ -1,11 +1,4 @@
1
- /**
2
- * External dependencies
3
- */
4
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
5
-
6
- /**
7
- * Internal dependencies
8
- */
9
2
  import {
10
3
  Card,
11
4
  CardHeader,
@@ -37,9 +30,9 @@ const meta: Meta< typeof Card > = {
37
30
  },
38
31
  docs: { canvas: { sourceState: 'shown' } },
39
32
  componentStatus: {
40
- status: 'use-with-caution',
33
+ status: 'not-recommended',
41
34
  whereUsed: 'global',
42
- notes: 'Design is in flux, and may differ from the version used in `@wordpress/dataviews`.',
35
+ notes: 'Use `Card` or `CollapsibleCard` from `@wordpress/ui` instead.',
43
36
  },
44
37
  },
45
38
  };
@@ -16,6 +16,7 @@ import { VStack } from '../../v-stack';
16
16
  import { HStack } from '../../h-stack';
17
17
 
18
18
  const meta: Meta< typeof CheckboxControl > = {
19
+ tags: [ 'manifest' ],
19
20
  component: CheckboxControl,
20
21
  title: 'Components/Selection & Input/Common/CheckboxControl',
21
22
  id: 'components-checkboxcontrol',
@@ -1,6 +1,5 @@
1
1
  @use "@wordpress/base-styles/colors" as *;
2
2
  @use "@wordpress/base-styles/variables" as *;
3
- @use "@wordpress/base-styles/z-index" as *;
4
3
 
5
4
  $color-palette-circle-size: 28px;
6
5
  $color-palette-circle-spacing: 12px;
@@ -9,6 +8,7 @@ $color-palette-circle-spacing: 12px;
9
8
  display: inline-block;
10
9
  width: 100%;
11
10
  min-width: 188px;
11
+ isolation: isolate;
12
12
 
13
13
  .components-circular-option-picker__custom-clear-wrapper {
14
14
  display: flex;
@@ -21,7 +21,7 @@ $color-palette-circle-spacing: 12px;
21
21
  flex-wrap: wrap;
22
22
  gap: $color-palette-circle-spacing;
23
23
  position: relative;
24
- z-index: z-index(".components-circular-option-picker__swatches");
24
+ z-index: 1;
25
25
  }
26
26
 
27
27
  // Make sure that the .components-circular-option-picker__swatches element
@@ -29,7 +29,7 @@ $color-palette-circle-spacing: 12px;
29
29
  // that the tooltip rendered when hovering an `Option` always appears on top.
30
30
  > *:not(.components-circular-option-picker__swatches) {
31
31
  position: relative;
32
- z-index: z-index("> *:not(.components-circular-option-picker__swatches)");
32
+ z-index: 0;
33
33
  }
34
34
  }
35
35
 
@@ -64,8 +64,9 @@ $color-palette-circle-spacing: 12px;
64
64
  bottom: 1px;
65
65
  right: 1px;
66
66
  border-radius: $radius-round;
67
+ // Sit below the option's color and selected-state UI.
68
+ z-index: -1;
67
69
  // Show a thin circular outline in Windows high contrast mode, otherwise the button is invisible.
68
- z-index: z-index(".components-circular-option-picker__option-wrapper::before");
69
70
  /* stylelint-disable-next-line function-url-quotes -- We need quotes for the data URL to use the SVG inline. */
70
71
  background: url('data:image/svg+xml,%3Csvg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M6 8V6H4v2h2zM8 8V6h2v2H8zM10 16H8v-2h2v2zM12 16v-2h2v2h-2zM12 18v-2h-2v2H8v2h2v-2h2zM14 18v2h-2v-2h2zM16 18h-2v-2h2v2z" fill="%23555D65"/%3E%3Cpath fill-rule="evenodd" clip-rule="evenodd" d="M18 18h2v-2h-2v-2h2v-2h-2v-2h2V8h-2v2h-2V8h-2v2h2v2h-2v2h2v2h2v2zm-2-4v-2h2v2h-2z" fill="%23555D65"/%3E%3Cpath d="M18 18v2h-2v-2h2z" fill="%23555D65"/%3E%3Cpath fill-rule="evenodd" clip-rule="evenodd" d="M8 10V8H6v2H4v2h2v2H4v2h2v2H4v2h2v2H4v2h2v-2h2v2h2v-2h2v2h2v-2h2v2h2v-2h2v2h2v-2h2v-2h-2v-2h2v-2h-2v-2h2v-2h-2v-2h2V8h-2V6h2V4h-2v2h-2V4h-2v2h-2V4h-2v2h-2V4h-2v2h2v2h-2v2H8zm0 2v-2H6v2h2zm2 0v-2h2v2h-2zm0 2v-2H8v2H6v2h2v2H6v2h2v2h2v-2h2v2h2v-2h2v2h2v-2h2v2h2v-2h-2v-2h2v-2h-2v-2h2v-2h-2v-2h2V8h-2V6h-2v2h-2V6h-2v2h-2v2h2v2h-2v2h-2z" fill="%23555D65"/%3E%3Cpath fill-rule="evenodd" clip-rule="evenodd" d="M4 0H2v2H0v2h2v2H0v2h2v2H0v2h2v2H0v2h2v2H0v2h2v2H0v2h2v2H0v2h2v-2h2v2h2v-2h2v2h2v-2h2v2h2v-2h2v2h2v-2h2v2h2v-2h2v2h2v-2h2v-2h-2v-2h2v-2h-2v-2h2v-2h-2v-2h2v-2h-2v-2h2V8h-2V6h2V4h-2V2h2V0h-2v2h-2V0h-2v2h-2V0h-2v2h-2V0h-2v2h-2V0h-2v2H8V0H6v2H4V0zm0 4V2H2v2h2zm2 0V2h2v2H6zm0 2V4H4v2H2v2h2v2H2v2h2v2H2v2h2v2H2v2h2v2H2v2h2v2h2v-2h2v2h2v-2h2v2h2v-2h2v2h2v-2h2v2h2v-2h2v2h2v-2h-2v-2h2v-2h-2v-2h2v-2h-2v-2h2v-2h-2v-2h2V8h-2V6h2V4h-2V2h-2v2h-2V2h-2v2h-2V2h-2v2h-2V2h-2v2H8v2H6z" fill="%23555D65"/%3E%3C/svg%3E');
71
72
  }
@@ -95,7 +96,7 @@ $color-palette-circle-spacing: 12px;
95
96
  &[aria-selected="true"] {
96
97
  box-shadow: inset 0 0 0 4px;
97
98
  position: relative;
98
- z-index: z-index(".components-circular-option-picker__option.is-pressed");
99
+ z-index: 1;
99
100
  overflow: visible;
100
101
 
101
102
  & + svg {
@@ -103,7 +104,8 @@ $color-palette-circle-spacing: 12px;
103
104
  left: 2px;
104
105
  top: 2px;
105
106
  border-radius: $radius-round;
106
- z-index: z-index(".components-circular-option-picker__option.is-pressed + svg");
107
+ // Render the selected checkmark above the selected option.
108
+ z-index: 2;
107
109
  pointer-events: none;
108
110
  }
109
111
  }
@@ -9,6 +9,7 @@ import type { Meta, StoryFn } from '@storybook/react-vite';
9
9
  import ColorIndicator from '..';
10
10
 
11
11
  const meta: Meta< typeof ColorIndicator > = {
12
+ tags: [ 'manifest' ],
12
13
  component: ColorIndicator,
13
14
  title: 'Components/Selection & Input/Color/ColorIndicator',
14
15
  id: 'components-colorindicator',
@@ -14,6 +14,7 @@ import { useState } from '@wordpress/element';
14
14
  import ColorPalette from '..';
15
15
 
16
16
  const meta: Meta< typeof ColorPalette > = {
17
+ tags: [ 'manifest' ],
17
18
  title: 'Components/Selection & Input/Color/ColorPalette',
18
19
  id: 'components-colorpalette',
19
20
  component: ColorPalette,
@@ -10,6 +10,7 @@ import { fn } from 'storybook/test';
10
10
  import { ColorPicker } from '../component';
11
11
 
12
12
  const meta: Meta< typeof ColorPicker > = {
13
+ tags: [ 'manifest' ],
13
14
  component: ColorPicker,
14
15
  title: 'Components/Selection & Input/Color/ColorPicker',
15
16
  id: 'components-colorpicker',
@@ -35,6 +35,7 @@ const countries = [
35
35
  ];
36
36
 
37
37
  const meta: Meta< typeof ComboboxControl > = {
38
+ tags: [ 'manifest' ],
38
39
  title: 'Components/Selection & Input/Common/ComboboxControl',
39
40
  id: 'components-comboboxcontrol',
40
41
  component: ComboboxControl,
@@ -16,6 +16,7 @@ import { Composite } from '..';
16
16
  import { Tooltip } from '../../tooltip';
17
17
 
18
18
  const meta: Meta< typeof Composite > = {
19
+ tags: [ 'manifest' ],
19
20
  title: 'Components/Utilities/Composite',
20
21
  id: 'components-composite',
21
22
  component: Composite,
@@ -24,7 +24,7 @@ const meta: Meta< typeof ConfirmDialog > = {
24
24
  control: false,
25
25
  },
26
26
  },
27
- tags: [ 'status-experimental' ],
27
+ tags: [ 'status-experimental', 'manifest' ],
28
28
  args: {
29
29
  onCancel: fn(),
30
30
  onConfirm: fn(),
@@ -15,6 +15,7 @@ import { useState } from '@wordpress/element';
15
15
  import CustomSelectControl from '..';
16
16
 
17
17
  const meta: Meta< typeof CustomSelectControl > = {
18
+ tags: [ 'manifest' ],
18
19
  title: 'Components/Selection & Input/Common/CustomSelectControl',
19
20
  component: CustomSelectControl,
20
21
  id: 'components-customselectcontrol',
@@ -18,6 +18,7 @@ import TextareaControl from '../../textarea-control/';
18
18
  import { VStack } from '../../v-stack/';
19
19
 
20
20
  const meta: Meta< typeof Disabled > = {
21
+ tags: [ 'manifest' ],
21
22
  title: 'Components/Utilities/Disabled',
22
23
  id: 'components-disabled',
23
24
  component: Disabled,
@@ -17,6 +17,7 @@ import DropZone from '..';
17
17
  const ICONS = { upload, media };
18
18
 
19
19
  const meta: Meta< typeof DropZone > = {
20
+ tags: [ 'manifest' ],
20
21
  component: DropZone,
21
22
  id: 'components-dropzone',
22
23
  title: 'Components/Selection & Input/File Upload/DropZone',