@wordpress/components 21.0.5 → 21.0.7

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 (87) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/build/autocomplete/index.js +11 -9
  3. package/build/autocomplete/index.js.map +1 -1
  4. package/build/circular-option-picker/index.js +14 -14
  5. package/build/circular-option-picker/index.js.map +1 -1
  6. package/build/color-palette/index.js +83 -30
  7. package/build/color-palette/index.js.map +1 -1
  8. package/build/color-palette/styles.js +3 -3
  9. package/build/color-palette/styles.js.map +1 -1
  10. package/build/color-palette/types.js +6 -0
  11. package/build/color-palette/types.js.map +1 -0
  12. package/build/font-size-picker/index.js +1 -1
  13. package/build/font-size-picker/index.js.map +1 -1
  14. package/build/form-token-field/index.js +12 -10
  15. package/build/form-token-field/index.js.map +1 -1
  16. package/build/navigator/navigator-provider/component.js +5 -3
  17. package/build/navigator/navigator-provider/component.js.map +1 -1
  18. package/build/navigator/navigator-screen/component.js +5 -3
  19. package/build/navigator/navigator-screen/component.js.map +1 -1
  20. package/build/popover/index.js +1 -26
  21. package/build/popover/index.js.map +1 -1
  22. package/build-module/autocomplete/index.js +10 -9
  23. package/build-module/autocomplete/index.js.map +1 -1
  24. package/build-module/circular-option-picker/index.js +14 -14
  25. package/build-module/circular-option-picker/index.js.map +1 -1
  26. package/build-module/color-palette/index.js +81 -28
  27. package/build-module/color-palette/index.js.map +1 -1
  28. package/build-module/color-palette/styles.js +3 -3
  29. package/build-module/color-palette/styles.js.map +1 -1
  30. package/build-module/color-palette/types.js +2 -0
  31. package/build-module/color-palette/types.js.map +1 -0
  32. package/build-module/font-size-picker/index.js +1 -1
  33. package/build-module/font-size-picker/index.js.map +1 -1
  34. package/build-module/form-token-field/index.js +11 -10
  35. package/build-module/form-token-field/index.js.map +1 -1
  36. package/build-module/navigator/navigator-provider/component.js +5 -3
  37. package/build-module/navigator/navigator-provider/component.js.map +1 -1
  38. package/build-module/navigator/navigator-screen/component.js +5 -3
  39. package/build-module/navigator/navigator-screen/component.js.map +1 -1
  40. package/build-module/popover/index.js +1 -26
  41. package/build-module/popover/index.js.map +1 -1
  42. package/build-types/border-control/types.d.ts +1 -1
  43. package/build-types/border-control/types.d.ts.map +1 -1
  44. package/build-types/circular-option-picker/index.d.ts +4 -24
  45. package/build-types/circular-option-picker/index.d.ts.map +1 -1
  46. package/build-types/color-palette/index.d.ts +33 -18
  47. package/build-types/color-palette/index.d.ts.map +1 -1
  48. package/build-types/color-palette/stories/index.d.ts +21 -0
  49. package/build-types/color-palette/stories/index.d.ts.map +1 -0
  50. package/build-types/color-palette/styles.d.ts +2 -1
  51. package/build-types/color-palette/styles.d.ts.map +1 -1
  52. package/build-types/color-palette/test/index.d.ts +2 -0
  53. package/build-types/color-palette/test/index.d.ts.map +1 -0
  54. package/build-types/color-palette/types.d.ts +86 -0
  55. package/build-types/color-palette/types.d.ts.map +1 -0
  56. package/build-types/confirm-dialog/component.d.ts +4 -4
  57. package/build-types/form-token-field/index.d.ts.map +1 -1
  58. package/build-types/navigator/navigator-provider/component.d.ts.map +1 -1
  59. package/build-types/navigator/navigator-screen/component.d.ts.map +1 -1
  60. package/build-types/navigator/types.d.ts +1 -0
  61. package/build-types/navigator/types.d.ts.map +1 -1
  62. package/build-types/popover/index.d.ts.map +1 -1
  63. package/build-types/popover/types.d.ts +0 -14
  64. package/build-types/popover/types.d.ts.map +1 -1
  65. package/package.json +2 -2
  66. package/src/autocomplete/index.js +18 -9
  67. package/src/border-control/types.ts +1 -1
  68. package/src/circular-option-picker/index.js +14 -20
  69. package/src/color-palette/README.md +51 -49
  70. package/src/color-palette/{index.js → index.tsx} +132 -51
  71. package/src/color-palette/stories/{index.js → index.tsx} +38 -27
  72. package/src/color-palette/{styles.js → styles.ts} +0 -0
  73. package/src/color-palette/test/__snapshots__/index.tsx.snap +270 -0
  74. package/src/color-palette/test/index.tsx +164 -0
  75. package/src/color-palette/types.ts +93 -0
  76. package/src/font-size-picker/index.js +1 -1
  77. package/src/form-token-field/index.tsx +21 -10
  78. package/src/form-token-field/test/index.tsx +167 -73
  79. package/src/navigator/navigator-provider/component.tsx +2 -0
  80. package/src/navigator/navigator-screen/component.tsx +9 -1
  81. package/src/navigator/types.ts +1 -0
  82. package/src/popover/README.md +3 -9
  83. package/src/popover/index.tsx +1 -25
  84. package/src/popover/types.ts +0 -14
  85. package/tsconfig.tsbuildinfo +1 -1
  86. package/src/color-palette/test/__snapshots__/index.js.snap +0 -1207
  87. package/src/color-palette/test/index.js +0 -118
@@ -2,12 +2,27 @@
2
2
  * External dependencies
3
3
  */
4
4
  import {
5
+ fireEvent,
5
6
  render,
6
7
  screen,
7
8
  within,
8
9
  getDefaultNormalizer,
9
10
  waitFor,
10
11
  } from '@testing-library/react';
12
+
13
+ /**
14
+ * WordPress dependencies
15
+ */
16
+ import {
17
+ BACKSPACE,
18
+ ENTER,
19
+ UP,
20
+ DOWN,
21
+ LEFT,
22
+ RIGHT,
23
+ DELETE,
24
+ ESCAPE,
25
+ } from '@wordpress/keycodes';
11
26
  import userEvent from '@testing-library/user-event';
12
27
  import type { ComponentProps } from 'react';
13
28
 
@@ -21,6 +36,54 @@ import { useState } from '@wordpress/element';
21
36
  */
22
37
  import FormTokenField from '../';
23
38
 
39
+ function triggerEnter( element: Element ) {
40
+ fireEvent.keyDown( element, {
41
+ keyCode: ENTER,
42
+ } );
43
+ }
44
+
45
+ function triggerBackspace( element: Element ) {
46
+ fireEvent.keyDown( element, {
47
+ keyCode: BACKSPACE,
48
+ } );
49
+ }
50
+
51
+ function triggerArrowRight( element: Element ) {
52
+ fireEvent.keyDown( element, {
53
+ keyCode: RIGHT,
54
+ } );
55
+ }
56
+
57
+ function triggerArrowLeft( element: Element ) {
58
+ fireEvent.keyDown( element, {
59
+ keyCode: LEFT,
60
+ } );
61
+ }
62
+
63
+ function triggerArrowUp( element: Element ) {
64
+ fireEvent.keyDown( element, {
65
+ keyCode: UP,
66
+ } );
67
+ }
68
+
69
+ function triggerArrowDown( element: Element ) {
70
+ fireEvent.keyDown( element, {
71
+ keyCode: DOWN,
72
+ } );
73
+ }
74
+
75
+ function triggerDelete( element: Element ) {
76
+ fireEvent.keyDown( element, {
77
+ keyCode: DELETE,
78
+ } );
79
+ }
80
+
81
+ function triggerEscape( element: Element ) {
82
+ fireEvent.keyDown( element, {
83
+ keyCode: ESCAPE,
84
+ } );
85
+ }
86
+
24
87
  const FormTokenFieldWithState = ( {
25
88
  onChange,
26
89
  value,
@@ -118,13 +181,15 @@ describe( 'FormTokenField', () => {
118
181
  const input = screen.getByRole( 'combobox' );
119
182
 
120
183
  // Add 'apple' token by typing it and pressing enter to tokenize it.
121
- await user.type( input, 'apple[Enter]' );
184
+ await user.type( input, 'apple' );
185
+ triggerEnter( input );
122
186
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
123
187
  expect( onChangeSpy ).toHaveBeenCalledWith( [ 'apple' ] );
124
188
  expectTokensToBeInTheDocument( [ 'apple' ] );
125
189
 
126
190
  // Add 'pear' token by typing it and pressing enter to tokenize it.
127
- await user.type( input, 'pear[Enter]' );
191
+ await user.type( input, 'pear' );
192
+ triggerEnter( input );
128
193
  expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
129
194
  expect( onChangeSpy ).toHaveBeenLastCalledWith( [
130
195
  'apple',
@@ -165,7 +230,8 @@ describe( 'FormTokenField', () => {
165
230
  const input = screen.getByRole( 'combobox' );
166
231
 
167
232
  // Add 'dragon fruit' token by typing it and pressing enter to tokenize it.
168
- await user.type( input, 'dragon fruit[Enter]' );
233
+ await user.type( input, 'dragon fruit' );
234
+ triggerEnter( input );
169
235
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
170
236
  expect( onChangeSpy ).toHaveBeenCalledWith( [ 'dragon fruit' ] );
171
237
  expectTokensToBeInTheDocument( [ 'dragon fruit' ] );
@@ -179,7 +245,8 @@ describe( 'FormTokenField', () => {
179
245
 
180
246
  // Add 'dragon fruit' token by typing it and pressing enter to tokenize it,
181
247
  // this time two separate tokens should be added
182
- await user.type( input, 'dragon fruit[Enter]' );
248
+ await user.type( input, 'dragon fruit' );
249
+ triggerEnter( input );
183
250
  expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
184
251
  expect( onChangeSpy ).toHaveBeenNthCalledWith( 2, [
185
252
  'dragon fruit',
@@ -216,10 +283,6 @@ describe( 'FormTokenField', () => {
216
283
  } );
217
284
 
218
285
  it( 'should remove the last token when pressing the backspace key', async () => {
219
- const user = userEvent.setup( {
220
- advanceTimers: jest.advanceTimersByTime,
221
- } );
222
-
223
286
  const onChangeSpy = jest.fn();
224
287
 
225
288
  render(
@@ -232,14 +295,15 @@ describe( 'FormTokenField', () => {
232
295
  const input = screen.getByRole( 'combobox' );
233
296
 
234
297
  // Press backspace to remove the last token ("mango")
235
- await user.type( input, '[Backspace]' );
298
+ input.focus();
299
+ triggerBackspace( input );
236
300
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
237
301
  expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'banana' ] );
238
302
  expectTokensToBeInTheDocument( [ 'banana' ] );
239
303
  expectTokensNotToBeInTheDocument( [ 'mango' ] );
240
304
 
241
305
  // Press backspace to remove the last token ("banana")
242
- await user.type( input, '[Backspace]' );
306
+ triggerBackspace( input );
243
307
  expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
244
308
  expect( onChangeSpy ).toHaveBeenLastCalledWith( [] );
245
309
  expectTokensNotToBeInTheDocument( [ 'banana', 'mango' ] );
@@ -343,23 +407,21 @@ describe( 'FormTokenField', () => {
343
407
  const input = screen.getByRole( 'combobox' );
344
408
 
345
409
  // Add 'guava' token by typing it and pressing enter to tokenize it.
346
- await user.type( input, 'guava[Enter]' );
410
+ await user.type( input, 'guava' );
411
+ triggerEnter( input );
347
412
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
348
413
  expect( onChangeSpy ).toHaveBeenCalledWith( [ 'papaya', 'guava' ] );
349
414
  expectTokensToBeInTheDocument( [ 'papaya', 'guava' ] );
350
415
 
351
416
  // Try to add a 'papaya' token by typing it and pressing enter to tokenize it,
352
417
  // but the token won't be added because it already exists.
353
- await user.type( input, 'papaya[Enter]' );
418
+ await user.type( input, 'papaya' );
419
+ triggerEnter( input );
354
420
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
355
421
  expectTokensToBeInTheDocument( [ 'papaya', 'guava' ] );
356
422
  } );
357
423
 
358
424
  it( 'should not add a new token if the text input is blank', async () => {
359
- const user = userEvent.setup( {
360
- advanceTimers: jest.advanceTimersByTime,
361
- } );
362
-
363
425
  const onChangeSpy = jest.fn();
364
426
 
365
427
  render(
@@ -372,7 +434,9 @@ describe( 'FormTokenField', () => {
372
434
  const input = screen.getByRole( 'combobox' );
373
435
 
374
436
  // Press enter on an empty input, no token gets added
375
- await user.type( input, '[Enter]' );
437
+ input.focus();
438
+ triggerEnter( input );
439
+
376
440
  expect( onChangeSpy ).not.toHaveBeenCalled();
377
441
  expectTokensToBeInTheDocument( [ 'melon' ] );
378
442
  } );
@@ -381,7 +445,6 @@ describe( 'FormTokenField', () => {
381
445
  const user = userEvent.setup( {
382
446
  advanceTimers: jest.advanceTimersByTime,
383
447
  } );
384
-
385
448
  const onChangeSpy = jest.fn();
386
449
 
387
450
  render(
@@ -402,12 +465,14 @@ describe( 'FormTokenField', () => {
402
465
 
403
466
  // Press "delete" to delete the token in front of the cursor, but since
404
467
  // there's no token in front of the cursor, nothing happens
405
- await user.type( input, '[Delete]' );
468
+ input.focus();
469
+ triggerDelete( input );
406
470
 
407
471
  // Pressing the right arrow doesn't move the cursor because there are no
408
472
  // tokens in front of it, and therefore pressing "delete" yields the same
409
473
  // result as before — no tokens are deleted.
410
- await user.type( input, '[ArrowRight][Delete]' );
474
+ triggerArrowRight( input );
475
+ triggerDelete( input );
411
476
 
412
477
  // Proof that so far, all keyboard interactions didn't delete any tokens.
413
478
  expect( onChangeSpy ).not.toHaveBeenCalled();
@@ -421,10 +486,12 @@ describe( 'FormTokenField', () => {
421
486
  // Press the left arrow 4 times, moving cursor between the "kiwi" and
422
487
  // "peach" tokens. Pressing the "delete" key will delete the "peach"
423
488
  // token, since it's in front of the cursor.
424
- await user.type(
425
- input,
426
- '[ArrowLeft][ArrowLeft][ArrowLeft][ArrowLeft][Delete]'
427
- );
489
+ triggerArrowLeft( input );
490
+ triggerArrowLeft( input );
491
+ triggerArrowLeft( input );
492
+ triggerArrowLeft( input );
493
+ triggerDelete( input );
494
+
428
495
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
429
496
  expect( onChangeSpy ).toHaveBeenCalledWith( [
430
497
  'peach',
@@ -440,20 +507,21 @@ describe( 'FormTokenField', () => {
440
507
 
441
508
  // Press backspace to delete the token before the cursor, but since
442
509
  // there's no token before the cursor, nothing happens
443
- await user.type( input, '[Backspace]' );
510
+ triggerBackspace( input );
444
511
 
445
512
  // Pressing the left arrow doesn't move the cursor because there are no
446
513
  // tokens before it, and therefore pressing backspace yields the same
447
514
  // result as before — no tokens are deleted.
448
- await user.type( input, '[ArrowLeft][Backspace]' );
449
-
515
+ triggerArrowLeft( input );
516
+ triggerBackspace( input );
450
517
  // Proof that pressing backspace hasn't caused any further token deletion.
451
518
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
452
519
 
453
520
  // Press the right arrow, moving cursor between the "kiwi" and
454
521
  // "nectarine" tokens. Pressing the "delete" key will delete the "nectarine"
455
522
  // token, since it's in front of the cursor.
456
- await user.type( input, '[ArrowRight][Delete]' );
523
+ triggerArrowRight( input );
524
+ triggerDelete( input );
457
525
  expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
458
526
  expect( onChangeSpy ).toHaveBeenCalledWith( [
459
527
  'peach',
@@ -464,7 +532,8 @@ describe( 'FormTokenField', () => {
464
532
 
465
533
  // Add 'starfruit' token while the cursor is in between the "peach" and
466
534
  // "coconut" tokens.
467
- await user.type( input, 'starfruit[Enter]' );
535
+ await user.type( input, 'starfruit' );
536
+ triggerEnter( input );
468
537
  expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
469
538
  expect( onChangeSpy ).toHaveBeenCalledWith( [
470
539
  'peach',
@@ -619,7 +688,8 @@ describe( 'FormTokenField', () => {
619
688
  const input = screen.getByRole( 'combobox' );
620
689
 
621
690
  // Add 'blueberry' token. The placeholder text should not be shown anymore
622
- await user.type( input, 'blueberry[Enter]' );
691
+ await user.type( input, 'blueberry' );
692
+ triggerEnter( input );
623
693
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
624
694
  expect( onChangeSpy ).toHaveBeenCalledWith( [ 'blueberry' ] );
625
695
  expectTokensToBeInTheDocument( [ 'blueberry' ] );
@@ -646,7 +716,8 @@ describe( 'FormTokenField', () => {
646
716
  const input = screen.getByRole( 'combobox' );
647
717
 
648
718
  // Add 'عربى' token by typing it and pressing enter to tokenize it.
649
- await user.type( input, 'عربى[Enter]' );
719
+ await user.type( input, 'عربى' );
720
+ triggerEnter( input );
650
721
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
651
722
  expect( onChangeSpy ).toHaveBeenCalledWith( [
652
723
  'français',
@@ -833,7 +904,7 @@ describe( 'FormTokenField', () => {
833
904
  ).toHaveLength( 0 );
834
905
 
835
906
  // Pressing the down arrow will select "Salmon"
836
- await user.keyboard( '[ArrowDown]' );
907
+ triggerArrowDown( input );
837
908
 
838
909
  expect(
839
910
  within( suggestionList ).getByRole( 'option', {
@@ -843,7 +914,7 @@ describe( 'FormTokenField', () => {
843
914
 
844
915
  // Pressing the up arrow will select "Neon" (the selection wraps around
845
916
  // the list)
846
- await user.keyboard( '[ArrowUp]' );
917
+ triggerArrowUp( input );
847
918
 
848
919
  expect(
849
920
  within( suggestionList ).getByRole( 'option', {
@@ -853,7 +924,8 @@ describe( 'FormTokenField', () => {
853
924
 
854
925
  // Pressing the down arrow twice will select "Carnation" (the selection
855
926
  // wraps around the list)
856
- await user.keyboard( '[ArrowDown][ArrowDown]' );
927
+ triggerArrowDown( input );
928
+ triggerArrowDown( input );
857
929
 
858
930
  expect(
859
931
  within( suggestionList ).getByRole( 'option', {
@@ -862,7 +934,7 @@ describe( 'FormTokenField', () => {
862
934
  ).toHaveAccessibleName( 'Carnation' );
863
935
 
864
936
  // Pressing enter will add "Carnation" as a token and close the suggestion list
865
- await user.keyboard( '[Enter]' );
937
+ triggerEnter( input );
866
938
 
867
939
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
868
940
  expect( onChangeSpy ).toHaveBeenCalledWith( [ 'Carnation' ] );
@@ -972,7 +1044,7 @@ describe( 'FormTokenField', () => {
972
1044
  expect( screen.getByRole( 'listbox' ) ).toBeVisible();
973
1045
 
974
1046
  // Pressing the ESC key will close the suggestion list
975
- await user.keyboard( '[Escape]' );
1047
+ triggerEscape( input );
976
1048
 
977
1049
  expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
978
1050
  expect( onChangeSpy ).not.toHaveBeenCalled();
@@ -1174,8 +1246,10 @@ describe( 'FormTokenField', () => {
1174
1246
  />
1175
1247
  );
1176
1248
 
1249
+ const input = screen.getByRole( 'combobox' );
1250
+
1177
1251
  // Type "woo". Matching suggestion will be "Wood"
1178
- await user.type( screen.getByRole( 'combobox' ), 'woo' );
1252
+ await user.type( input, 'woo' );
1179
1253
 
1180
1254
  // The `__experimentalRenderItem` only affects the rendered suggestion,
1181
1255
  // but doesn't change the underlying data `value`, nor the value
@@ -1184,7 +1258,8 @@ describe( 'FormTokenField', () => {
1184
1258
  'Suggestion: Wood',
1185
1259
  ] );
1186
1260
 
1187
- await user.keyboard( '[ArrowDown][Enter]' );
1261
+ triggerArrowDown( input );
1262
+ triggerEnter( input );
1188
1263
 
1189
1264
  expectTokensToBeInTheDocument( [ 'Wood' ] );
1190
1265
  } );
@@ -1213,7 +1288,8 @@ describe( 'FormTokenField', () => {
1213
1288
 
1214
1289
  const input = screen.getByRole( 'combobox' );
1215
1290
 
1216
- await user.type( input, 'Italy[Enter]' );
1291
+ await user.type( input, 'Italy' );
1292
+ triggerEnter( input );
1217
1293
 
1218
1294
  expect( onChangeSpy ).not.toHaveBeenCalled();
1219
1295
 
@@ -1318,12 +1394,13 @@ describe( 'FormTokenField', () => {
1318
1394
  const input = screen.getByRole( 'combobox' );
1319
1395
 
1320
1396
  // Press enter on an empty input, no token gets added
1321
- await user.type( input, '[Enter]' );
1397
+ triggerEnter( input );
1322
1398
  expect( onChangeSpy ).not.toHaveBeenCalled();
1323
1399
  expectTokensToBeInTheDocument( [ 'potato' ] );
1324
1400
 
1325
1401
  // Add the "carrot" token - white space gets trimmed
1326
- await user.type( input, ' carrot [Enter]' );
1402
+ await user.type( input, ' carrot ' );
1403
+ triggerEnter( input );
1327
1404
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1328
1405
  expect( onChangeSpy ).toHaveBeenCalledWith( [
1329
1406
  'potato',
@@ -1333,12 +1410,14 @@ describe( 'FormTokenField', () => {
1333
1410
 
1334
1411
  // Press enter on an input containing a duplicate token but surrounded by
1335
1412
  // white space, no token gets added
1336
- await user.type( input, ' potato [Enter]' );
1413
+ await user.type( input, ' potato ' );
1414
+ triggerEnter( input );
1337
1415
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1338
1416
  expectTokensToBeInTheDocument( [ 'potato', 'carrot' ] );
1339
1417
 
1340
1418
  // Press enter on an input containing only spaces, no token gets added
1341
- await user.type( input, ' [Enter]' );
1419
+ await user.type( input, ' ' );
1420
+ triggerEnter( input );
1342
1421
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1343
1422
  expectTokensToBeInTheDocument( [ 'potato', 'carrot' ] );
1344
1423
 
@@ -1353,7 +1432,8 @@ describe( 'FormTokenField', () => {
1353
1432
  // If a custom `saveTransform` function is passed, it will be the new
1354
1433
  // function's duty to trim the whitespace if necessary.
1355
1434
  await user.clear( input );
1356
- await user.type( input, ' parnsnip [Enter]' );
1435
+ await user.type( input, ' parnsnip ' );
1436
+ triggerEnter( input );
1357
1437
  expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
1358
1438
  expect( onChangeSpy ).toHaveBeenCalledWith( [
1359
1439
  'potato',
@@ -1413,7 +1493,8 @@ describe( 'FormTokenField', () => {
1413
1493
  // The saveTransform function will change its value to "medium jacket"
1414
1494
  // when tokenizing it, thus affecting both the onChange callback and
1415
1495
  // the text rendered in the document.
1416
- await user.type( input, 'small jacket[Enter]' );
1496
+ await user.type( input, 'small jacket' );
1497
+ triggerEnter( input );
1417
1498
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1418
1499
  expect( onChangeSpy ).toHaveBeenCalledWith( [
1419
1500
  'small trousers',
@@ -1464,7 +1545,8 @@ describe( 'FormTokenField', () => {
1464
1545
 
1465
1546
  // Selecting the suggestion will add the transformed value as a token,
1466
1547
  // since the `saveTransform` function will be applied before tokenizing.
1467
- await user.keyboard( '[ArrowDown][Enter]' );
1548
+ triggerArrowDown( input );
1549
+ triggerEnter( input );
1468
1550
 
1469
1551
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1470
1552
  expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'Free food' ] );
@@ -1511,7 +1593,8 @@ describe( 'FormTokenField', () => {
1511
1593
  // The displayTransform function will change its displayed value to
1512
1594
  // "light red", but the onChange callback will still receive "dark red" as
1513
1595
  // part of the component's new value.
1514
- await user.type( input, 'dark red[Enter]' );
1596
+ await user.type( input, 'dark red' );
1597
+ triggerEnter( input );
1515
1598
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1516
1599
  expect( onChangeSpy ).toHaveBeenCalledWith( [
1517
1600
  'dark blue',
@@ -1560,7 +1643,8 @@ describe( 'FormTokenField', () => {
1560
1643
  'cold tea',
1561
1644
  ] );
1562
1645
 
1563
- await user.keyboard( '[ArrowDown][Enter]' );
1646
+ triggerArrowDown( input );
1647
+ triggerEnter( input );
1564
1648
 
1565
1649
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1566
1650
  expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'Hot coffee' ] );
@@ -1642,7 +1726,8 @@ describe( 'FormTokenField', () => {
1642
1726
  const input = screen.getByRole( 'combobox' );
1643
1727
 
1644
1728
  // Add 'cherry' token by typing it and pressing enter to tokenize it.
1645
- await user.type( input, 'cherry[Enter]' );
1729
+ await user.type( input, 'cherry' );
1730
+ triggerEnter( input );
1646
1731
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1647
1732
  expect( onChangeSpy ).toHaveBeenCalledWith( [ 'cherry' ] );
1648
1733
  expectTokensToBeInTheDocument( [ 'cherry' ] );
@@ -1659,14 +1744,16 @@ describe( 'FormTokenField', () => {
1659
1744
  // Note that the any token added before is still around, even if it
1660
1745
  // wouldn't pass the newly added validation — this is because the
1661
1746
  // validation happens when the input\'s value gets tokenized.
1662
- await user.type( input, 'cranberry[Enter]' );
1747
+ await user.type( input, 'cranberry' );
1748
+ triggerEnter( input );
1663
1749
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1664
1750
  expectTokensToBeInTheDocument( [ 'cherry' ] );
1665
1751
  expectTokensNotToBeInTheDocument( [ 'cranberry' ] );
1666
1752
 
1667
1753
  // Retry, this time with capital letter. The value should be added.
1668
1754
  await user.clear( input );
1669
- await user.type( input, 'Cranberry[Enter]' );
1755
+ await user.type( input, 'Cranberry' );
1756
+ triggerEnter( input );
1670
1757
  expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
1671
1758
  expectTokensToBeInTheDocument( [ 'cherry', 'Cranberry' ] );
1672
1759
  } );
@@ -1694,7 +1781,8 @@ describe( 'FormTokenField', () => {
1694
1781
 
1695
1782
  // Try to add the 'hexagon' token, but because the number of tokens already
1696
1783
  // matches `maxLength`, the token won't be added.
1697
- await user.type( input, 'hexagon[Enter]' );
1784
+ await user.type( input, 'hexagon' );
1785
+ triggerEnter( input );
1698
1786
  expect( onChangeSpy ).toHaveBeenCalledTimes( 0 );
1699
1787
  expectTokensToBeInTheDocument( [ 'square', 'triangle', 'circle' ] );
1700
1788
  expectTokensNotToBeInTheDocument( [ 'hexagon' ] );
@@ -1702,7 +1790,7 @@ describe( 'FormTokenField', () => {
1702
1790
  // Delete the last token ("circle"), in order to make space for the
1703
1791
  // hexagon token
1704
1792
  await user.clear( input );
1705
- await user.keyboard( '[Backspace]' );
1793
+ triggerBackspace( input );
1706
1794
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1707
1795
  expect( onChangeSpy ).toHaveBeenLastCalledWith( [
1708
1796
  'square',
@@ -1714,7 +1802,8 @@ describe( 'FormTokenField', () => {
1714
1802
  // Try to add the 'hexagon' token again. This time, the token will be
1715
1803
  // added because the current number of tokens is below the `maxLength`
1716
1804
  // threshold.
1717
- await user.type( input, 'hexagon[Enter]' );
1805
+ await user.type( input, 'hexagon' );
1806
+ triggerEnter( input );
1718
1807
  expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
1719
1808
  expect( onChangeSpy ).toHaveBeenLastCalledWith( [
1720
1809
  'square',
@@ -1756,7 +1845,12 @@ describe( 'FormTokenField', () => {
1756
1845
 
1757
1846
  const input = screen.getByRole( 'combobox' );
1758
1847
 
1759
- await user.type( input, 'cube[Enter]sphere[Enter]cylinder[Enter]' );
1848
+ await user.type( input, 'cube' );
1849
+ triggerEnter( input );
1850
+ await user.type( input, 'sphere' );
1851
+ triggerEnter( input );
1852
+ await user.type( input, 'cylinder' );
1853
+ triggerEnter( input );
1760
1854
  expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
1761
1855
  expect( onChangeSpy ).toHaveBeenLastCalledWith( [
1762
1856
  'cube',
@@ -1780,7 +1874,8 @@ describe( 'FormTokenField', () => {
1780
1874
 
1781
1875
  // Try to add the 'pyramid' token, but because the number of tokens already
1782
1876
  // exceeds `maxLength`, the token won't be added.
1783
- await user.type( input, 'pyramid[Enter]' );
1877
+ await user.type( input, 'pyramid' );
1878
+ triggerEnter( input );
1784
1879
  expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
1785
1880
  expectTokensToBeInTheDocument( [ 'cube', 'sphere', 'cylinder' ] );
1786
1881
  expectTokensNotToBeInTheDocument( [ 'pyramid' ] );
@@ -1802,7 +1897,8 @@ describe( 'FormTokenField', () => {
1802
1897
  const input = screen.getByRole( 'combobox' );
1803
1898
 
1804
1899
  // Add 'sun' token by typing it and pressing enter to tokenize it.
1805
- await user.type( input, 'sun[Enter]' );
1900
+ await user.type( input, 'sun' );
1901
+ triggerEnter( input );
1806
1902
  expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1807
1903
  expect( onChangeSpy ).toHaveBeenCalledWith( [ 'sun' ] );
1808
1904
  expectTokensToBeInTheDocument( [ 'sun' ] );
@@ -1876,7 +1972,8 @@ describe( 'FormTokenField', () => {
1876
1972
  const input = screen.getByRole( 'combobox' );
1877
1973
 
1878
1974
  // Add 'cat' token, check that the aria-live region has been updated.
1879
- await user.type( input, 'cat[Enter]' );
1975
+ await user.type( input, 'cat' );
1976
+ triggerEnter( input );
1880
1977
 
1881
1978
  expect( screen.getByText( defaultMessages.added ) ).toHaveAttribute(
1882
1979
  'aria-live',
@@ -1894,7 +1991,8 @@ describe( 'FormTokenField', () => {
1894
1991
  const input = screen.getByRole( 'combobox' );
1895
1992
 
1896
1993
  // Add 'dog' token, check that the aria-live region has been updated.
1897
- await user.type( input, 'dog[Enter]' );
1994
+ await user.type( input, 'dog' );
1995
+ triggerEnter( input );
1898
1996
 
1899
1997
  expect( screen.getByText( customMessages.added ) ).toHaveAttribute(
1900
1998
  'aria-live',
@@ -1903,16 +2001,13 @@ describe( 'FormTokenField', () => {
1903
2001
  } );
1904
2002
 
1905
2003
  it( 'should announce to assistive technology the removal of a token', async () => {
1906
- const user = userEvent.setup( {
1907
- advanceTimers: jest.advanceTimersByTime,
1908
- } );
1909
-
1910
2004
  render( <FormTokenFieldWithState initialValue={ [ 'horse' ] } /> );
1911
2005
 
1912
2006
  const input = screen.getByRole( 'combobox' );
1913
2007
 
1914
2008
  // Delete "horse" token
1915
- await user.type( input, '[Backspace]' );
2009
+ input.focus();
2010
+ triggerBackspace( input );
1916
2011
 
1917
2012
  expect(
1918
2013
  screen.getByText( defaultMessages.removed )
@@ -1920,10 +2015,6 @@ describe( 'FormTokenField', () => {
1920
2015
  } );
1921
2016
 
1922
2017
  it( 'should announce to assistive technology the removal of a token with a custom message', async () => {
1923
- const user = userEvent.setup( {
1924
- advanceTimers: jest.advanceTimersByTime,
1925
- } );
1926
-
1927
2018
  render(
1928
2019
  <FormTokenFieldWithState
1929
2020
  initialValue={ [ 'donkey' ] }
@@ -1934,7 +2025,8 @@ describe( 'FormTokenField', () => {
1934
2025
  const input = screen.getByRole( 'combobox' );
1935
2026
 
1936
2027
  // Delete "donkey" token
1937
- await user.type( input, '[Backspace]' );
2028
+ input.focus();
2029
+ triggerBackspace( input );
1938
2030
 
1939
2031
  expect(
1940
2032
  screen.getByText( customMessages.removed )
@@ -1956,7 +2048,8 @@ describe( 'FormTokenField', () => {
1956
2048
 
1957
2049
  // Try to add "eagle" token, which won't be added because of the
1958
2050
  // __experimentalValidateInput prop.
1959
- await user.type( input, 'eagle[Enter]' );
2051
+ await user.type( input, 'eagle' );
2052
+ triggerEnter( input );
1960
2053
 
1961
2054
  expect(
1962
2055
  screen.getByText( defaultMessages.__experimentalInvalid )
@@ -1979,7 +2072,8 @@ describe( 'FormTokenField', () => {
1979
2072
 
1980
2073
  // Try to add "crocodile" token, which won't be added because of the
1981
2074
  // __experimentalValidateInput prop.
1982
- await user.type( input, 'crocodile[Enter]' );
2075
+ await user.type( input, 'crocodile' );
2076
+ triggerEnter( input );
1983
2077
 
1984
2078
  expect(
1985
2079
  screen.getByText( customMessages.__experimentalInvalid )
@@ -2085,7 +2179,7 @@ describe( 'FormTokenField', () => {
2085
2179
 
2086
2180
  // Select the "Pine" suggestion
2087
2181
  await user.click( input );
2088
- await user.keyboard( '[ArrowDown]' );
2182
+ triggerArrowDown( input );
2089
2183
 
2090
2184
  const pineSuggestion = within( suggestionList ).getByRole(
2091
2185
  'option',
@@ -2115,7 +2209,7 @@ describe( 'FormTokenField', () => {
2115
2209
  );
2116
2210
 
2117
2211
  // Add the suggestion, which hides the list
2118
- await user.keyboard( '[Enter]' );
2212
+ triggerEnter( input );
2119
2213
 
2120
2214
  expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
2121
2215
 
@@ -49,6 +49,7 @@ function NavigatorProvider(
49
49
  ...options,
50
50
  path,
51
51
  isBack: false,
52
+ hasRestoredFocus: false,
52
53
  },
53
54
  ] );
54
55
  },
@@ -62,6 +63,7 @@ function NavigatorProvider(
62
63
  {
63
64
  ...locationHistory[ locationHistory.length - 2 ],
64
65
  isBack: true,
66
+ hasRestoredFocus: false,
65
67
  },
66
68
  ] );
67
69
  }
@@ -79,7 +79,13 @@ function NavigatorScreen( props: Props, forwardedRef: ForwardedRef< any > ) {
79
79
  // - if the current location is not the initial one (to avoid moving focus on page load)
80
80
  // - when the screen becomes visible
81
81
  // - if the wrapper ref has been assigned
82
- if ( isInitialLocation || ! isMatch || ! wrapperRef.current ) {
82
+ // - if focus hasn't already been restored for the current location
83
+ if (
84
+ isInitialLocation ||
85
+ ! isMatch ||
86
+ ! wrapperRef.current ||
87
+ location.hasRestoredFocus
88
+ ) {
83
89
  return;
84
90
  }
85
91
 
@@ -103,10 +109,12 @@ function NavigatorScreen( props: Props, forwardedRef: ForwardedRef< any > ) {
103
109
  elementToFocus = firstTabbable ?? wrapperRef.current;
104
110
  }
105
111
 
112
+ location.hasRestoredFocus = true;
106
113
  elementToFocus.focus();
107
114
  }, [
108
115
  isInitialLocation,
109
116
  isMatch,
117
+ location.hasRestoredFocus,
110
118
  location.isBack,
111
119
  previousLocation?.focusTargetSelector,
112
120
  ] );
@@ -11,6 +11,7 @@ export type NavigatorLocation = NavigateOptions & {
11
11
  isInitial?: boolean;
12
12
  isBack?: boolean;
13
13
  path?: string;
14
+ hasRestoredFocus?: boolean;
14
15
  };
15
16
 
16
17
  export type NavigatorContext = {