@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
@@ -18,6 +18,14 @@ const delay = ( delayInMs: number ) => {
18
18
  return new Promise( ( resolve ) => setTimeout( resolve, delayInMs ) );
19
19
  };
20
20
 
21
+ const waitForFocusedMenu = () =>
22
+ waitFor( () => expect( screen.getByRole( 'menu' ) ).toHaveFocus() );
23
+
24
+ const waitForFocusedMenuItem = ( name: string ) =>
25
+ waitFor( () =>
26
+ expect( screen.getByRole( 'menuitem', { name } ) ).toHaveFocus()
27
+ );
28
+
21
29
  // Open dropdown => open menu
22
30
  // Submenu trigger item => open submenu
23
31
 
@@ -114,7 +122,7 @@ describe( 'Menu', () => {
114
122
  await click( toggleButton );
115
123
 
116
124
  // Menu open, focus is on the menu wrapper
117
- expect( screen.getByRole( 'menu' ) ).toHaveFocus();
125
+ await waitForFocusedMenu();
118
126
  } );
119
127
 
120
128
  it( 'should open and focus the first item when pressing the arrow down key on the trigger', async () => {
@@ -145,9 +153,7 @@ describe( 'Menu', () => {
145
153
 
146
154
  // Menu open, focus is on the first focusable item
147
155
  // (disabled items are still focusable and accessible)
148
- expect(
149
- screen.getByRole( 'menuitem', { name: 'First item' } )
150
- ).toHaveFocus();
156
+ await waitForFocusedMenuItem( 'First item' );
151
157
  } );
152
158
 
153
159
  it( 'should open and focus the first item when pressing the space key on the trigger', async () => {
@@ -178,9 +184,7 @@ describe( 'Menu', () => {
178
184
 
179
185
  // Menu open, focus is on the first focusable item
180
186
  // (disabled items are still focusable and accessible
181
- expect(
182
- screen.getByRole( 'menuitem', { name: 'First item' } )
183
- ).toHaveFocus();
187
+ await waitForFocusedMenuItem( 'First item' );
184
188
  } );
185
189
 
186
190
  it( 'should close when pressing the escape key', async () => {
@@ -201,7 +205,7 @@ describe( 'Menu', () => {
201
205
 
202
206
  // Focuses menu on mouse click, focuses first item on keyboard press
203
207
  // Can be changed with a custom useEffect
204
- expect( screen.getByRole( 'menu' ) ).toHaveFocus();
208
+ await waitForFocusedMenu();
205
209
 
206
210
  // Pressing esc will close the menu and move focus to the toggle
207
211
  await press.Escape();
@@ -349,9 +353,7 @@ describe( 'Menu', () => {
349
353
  );
350
354
 
351
355
  // The menu is focused automatically when `defaultOpen` is set.
352
- await waitFor( () =>
353
- expect( screen.getByRole( 'menu' ) ).toHaveFocus()
354
- );
356
+ await waitForFocusedMenu();
355
357
 
356
358
  // Arrow up/down selects menu items
357
359
  // The selection wraps around from last to first and viceversa
@@ -391,18 +393,13 @@ describe( 'Menu', () => {
391
393
  ).toHaveFocus();
392
394
 
393
395
  // Arrow right/left can be used to enter/leave submenus
396
+ // (focus crosses menu contexts, so wait for it to settle)
394
397
  await press.ArrowRight();
395
- expect(
396
- screen.getByRole( 'menuitem', {
397
- name: 'Submenu item 1',
398
- } )
399
- ).toHaveFocus();
398
+ await waitForFocusedMenuItem( 'Submenu item 1' );
400
399
 
401
400
  await press.ArrowDown();
402
401
  expect(
403
- screen.getByRole( 'menuitem', {
404
- name: 'Submenu item 2',
405
- } )
402
+ screen.getByRole( 'menuitem', { name: 'Submenu item 2' } )
406
403
  ).toHaveFocus();
407
404
 
408
405
  await press.ArrowLeft();
@@ -414,11 +411,7 @@ describe( 'Menu', () => {
414
411
 
415
412
  // Spacebar or enter key can also be used to enter a submenu
416
413
  await press.Enter();
417
- expect(
418
- screen.getByRole( 'menuitem', {
419
- name: 'Submenu item 1',
420
- } )
421
- ).toHaveFocus();
414
+ await waitForFocusedMenuItem( 'Submenu item 1' );
422
415
 
423
416
  await press.ArrowLeft();
424
417
  expect(
@@ -428,11 +421,7 @@ describe( 'Menu', () => {
428
421
  ).toHaveFocus();
429
422
 
430
423
  await press.Space();
431
- expect(
432
- screen.getByRole( 'menuitem', {
433
- name: 'Submenu item 1',
434
- } )
435
- ).toHaveFocus();
424
+ await waitForFocusedMenuItem( 'Submenu item 1' );
436
425
 
437
426
  await press.ArrowLeft();
438
427
  expect(
@@ -886,7 +875,7 @@ describe( 'Menu', () => {
886
875
  );
887
876
 
888
877
  // Menu open, focus is on the menu wrapper
889
- expect( screen.getByRole( 'menu' ) ).toHaveFocus();
878
+ await waitForFocusedMenu();
890
879
 
891
880
  expect(
892
881
  screen.queryByRole( 'button', {
@@ -916,18 +905,19 @@ describe( 'Menu', () => {
916
905
  );
917
906
 
918
907
  // Menu open, focus is on the menu wrapper
919
- expect( screen.getByRole( 'menu' ) ).toHaveFocus();
908
+ await waitForFocusedMenu();
920
909
 
921
910
  // Menu is not modal, therefore the outer button is part of the
922
911
  // accessibility tree and can be found.
923
912
  const outerButton = screen.getByRole( 'button', {
924
913
  name: 'Button outside of dropdown',
925
914
  } );
915
+ expect( outerButton ).toBeVisible();
926
916
 
927
917
  // The outer button can be focused by pressing tab. Doing so will cause
928
918
  // the Menu to close.
929
919
  await press.Tab();
930
- expect( outerButton ).toBeInTheDocument();
920
+ expect( outerButton ).toBeVisible();
931
921
  expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument();
932
922
  } );
933
923
  } );
@@ -1068,7 +1058,7 @@ describe( 'Menu', () => {
1068
1058
  name: 'Open dropdown',
1069
1059
  } )
1070
1060
  );
1071
- expect( screen.getByRole( 'menu' ) ).toHaveFocus();
1061
+ await waitForFocusedMenu();
1072
1062
 
1073
1063
  // Type "tw", it should match and focus the item with content "Two"
1074
1064
  await type( 'tw' );
@@ -1104,7 +1094,7 @@ describe( 'Menu', () => {
1104
1094
  name: 'Open dropdown',
1105
1095
  } )
1106
1096
  );
1107
- expect( screen.getByRole( 'menu' ) ).toHaveFocus();
1097
+ await waitForFocusedMenu();
1108
1098
 
1109
1099
  // Type a string that doesn't match any items. Focus shouldn't move.
1110
1100
  await type( 'abc' );
@@ -16,6 +16,7 @@ import MenuItem from '../../menu-item';
16
16
  import MenuItemsChoice from '../../menu-items-choice';
17
17
 
18
18
  const meta: Meta< typeof MenuGroup > = {
19
+ tags: [ 'manifest' ],
19
20
  title: 'Components/Actions/MenuGroup',
20
21
  component: MenuGroup,
21
22
  id: 'components-menugroup',
@@ -16,6 +16,7 @@ import MenuItem from '..';
16
16
  import Shortcut from '../../shortcut';
17
17
 
18
18
  const meta: Meta< typeof MenuItem > = {
19
+ tags: [ 'manifest' ],
19
20
  component: MenuItem,
20
21
  title: 'Components/Actions/MenuItem',
21
22
  id: 'components-menuitem',
@@ -15,6 +15,7 @@ import MenuItemsChoice from '..';
15
15
  import MenuGroup from '../../menu-group';
16
16
 
17
17
  const meta: Meta< typeof MenuItemsChoice > = {
18
+ tags: [ 'manifest' ],
18
19
  component: MenuItemsChoice,
19
20
  title: 'Components/Actions/MenuItemsChoice',
20
21
  id: 'components-menuitemschoice',
@@ -32,7 +32,7 @@ import LinkRelIcon from './link-rel';
32
32
 
33
33
  import styles from './style.scss';
34
34
 
35
- const NEW_TAB_REL = 'noreferrer noopener';
35
+ const NEW_TAB_REL = 'noopener';
36
36
  function LinkSettings( {
37
37
  // Control link settings `BottomSheet` visibility
38
38
  isVisible,
@@ -18,6 +18,7 @@ import Modal from '../';
18
18
  import type { ModalProps } from '../types';
19
19
 
20
20
  const meta: Meta< typeof Modal > = {
21
+ tags: [ 'manifest' ],
21
22
  component: Modal,
22
23
  title: 'Components/Overlays/Modal',
23
24
  id: 'components-modal',
@@ -6,7 +6,8 @@ import type { ForwardedRef } from 'react';
6
6
  /**
7
7
  * WordPress dependencies
8
8
  */
9
- import { Component, forwardRef } from '@wordpress/element';
9
+ import { forwardRef, useRef, useEffect, useCallback } from '@wordpress/element';
10
+ import { useMergeRefs } from '@wordpress/compose';
10
11
  import { focus } from '@wordpress/dom';
11
12
 
12
13
  /**
@@ -28,163 +29,141 @@ function cycleValue( value: number, total: number, offset: number ) {
28
29
  return nextValue;
29
30
  }
30
31
 
31
- class NavigableContainer extends Component< NavigableContainerProps > {
32
- container?: HTMLDivElement;
33
-
34
- constructor( args: NavigableContainerProps ) {
35
- super( args );
36
- this.onKeyDown = this.onKeyDown.bind( this );
37
- this.bindContainer = this.bindContainer.bind( this );
38
-
39
- this.getFocusableContext = this.getFocusableContext.bind( this );
40
- this.getFocusableIndex = this.getFocusableIndex.bind( this );
41
- }
42
-
43
- componentDidMount() {
44
- if ( ! this.container ) {
45
- return;
46
- }
47
-
48
- // We use DOM event listeners instead of React event listeners
49
- // because we want to catch events from the underlying DOM tree
50
- // The React Tree can be different from the DOM tree when using
51
- // portals. Block Toolbars for instance are rendered in a separate
52
- // React Trees.
53
- this.container.addEventListener( 'keydown', this.onKeyDown );
54
- }
55
-
56
- componentWillUnmount() {
57
- if ( ! this.container ) {
58
- return;
59
- }
60
-
61
- this.container.removeEventListener( 'keydown', this.onKeyDown );
62
- }
63
-
64
- bindContainer( ref: HTMLDivElement ) {
65
- const { forwardedRef } = this.props;
66
- this.container = ref;
32
+ function UnforwardedNavigableContainer(
33
+ {
34
+ children,
35
+ stopNavigationEvents,
36
+ eventToOffset,
37
+ onNavigate = noop,
38
+ onKeyDown,
39
+ cycle = true,
40
+ onlyBrowserTabstops,
41
+ ...restProps
42
+ }: NavigableContainerProps,
43
+ ref: ForwardedRef< HTMLDivElement >
44
+ ) {
45
+ const containerRef = useRef< HTMLDivElement | null >( null );
46
+
47
+ const getFocusableIndex = useCallback(
48
+ ( focusables: Element[], target: Element ) => {
49
+ return focusables.indexOf( target );
50
+ },
51
+ []
52
+ );
53
+
54
+ const getFocusableContext = useCallback(
55
+ ( target: Element ) => {
56
+ if ( ! containerRef.current ) {
57
+ return null;
58
+ }
67
59
 
68
- if ( typeof forwardedRef === 'function' ) {
69
- forwardedRef( ref );
70
- } else if ( forwardedRef && 'current' in forwardedRef ) {
71
- forwardedRef.current = ref;
72
- }
73
- }
60
+ const finder = onlyBrowserTabstops
61
+ ? focus.tabbable
62
+ : focus.focusable;
63
+ const focusables = finder.find( containerRef.current );
74
64
 
75
- getFocusableContext( target: Element ) {
76
- if ( ! this.container ) {
65
+ const index = getFocusableIndex( focusables, target );
66
+ if ( index > -1 && target ) {
67
+ return { index, target, focusables };
68
+ }
77
69
  return null;
78
- }
70
+ },
71
+ [ onlyBrowserTabstops, getFocusableIndex ]
72
+ );
79
73
 
80
- const { onlyBrowserTabstops } = this.props;
81
- const finder = onlyBrowserTabstops ? focus.tabbable : focus.focusable;
82
- const focusables = finder.find( this.container );
83
-
84
- const index = this.getFocusableIndex( focusables, target );
85
- if ( index > -1 && target ) {
86
- return { index, target, focusables };
74
+ useEffect( () => {
75
+ const container = containerRef.current;
76
+ if ( ! container ) {
77
+ return;
87
78
  }
88
- return null;
89
- }
90
79
 
91
- getFocusableIndex( focusables: Element[], target: Element ) {
92
- return focusables.indexOf( target );
93
- }
94
-
95
- onKeyDown( event: KeyboardEvent ) {
96
- if ( this.props.onKeyDown ) {
97
- this.props.onKeyDown( event );
98
- }
80
+ function handleKeyDown( event: KeyboardEvent ) {
81
+ if ( onKeyDown ) {
82
+ onKeyDown( event );
83
+ }
99
84
 
100
- const { getFocusableContext } = this;
101
- const {
102
- cycle = true,
103
- eventToOffset,
104
- onNavigate = noop,
105
- stopNavigationEvents,
106
- } = this.props;
107
-
108
- const offset = eventToOffset( event );
109
-
110
- // eventToOffset returns undefined if the event is not handled by the component.
111
- if ( offset !== undefined && stopNavigationEvents ) {
112
- // Prevents arrow key handlers bound to the document directly interfering.
113
- event.stopImmediatePropagation();
114
-
115
- // When navigating a collection of items, prevent scroll containers
116
- // from scrolling. The preventDefault also prevents Voiceover from
117
- // 'handling' the event, as voiceover will try to use arrow keys
118
- // for highlighting text.
119
- const targetRole = (
120
- event.target as HTMLDivElement | null
121
- )?.getAttribute( 'role' );
122
- const targetHasMenuItemRole =
123
- !! targetRole && MENU_ITEM_ROLES.includes( targetRole );
124
-
125
- if ( targetHasMenuItemRole ) {
126
- event.preventDefault();
85
+ const offset = eventToOffset( event );
86
+
87
+ // eventToOffset returns undefined if the event is not handled by the component.
88
+ if ( offset !== undefined && stopNavigationEvents ) {
89
+ // Prevents arrow key handlers bound to the document directly interfering.
90
+ event.stopImmediatePropagation();
91
+
92
+ // When navigating a collection of items, prevent scroll containers
93
+ // from scrolling. The preventDefault also prevents Voiceover from
94
+ // 'handling' the event, as voiceover will try to use arrow keys
95
+ // for highlighting text.
96
+ const targetRole = (
97
+ event.target as HTMLDivElement | null
98
+ )?.getAttribute( 'role' );
99
+ const targetHasMenuItemRole =
100
+ !! targetRole && MENU_ITEM_ROLES.includes( targetRole );
101
+
102
+ if ( targetHasMenuItemRole ) {
103
+ event.preventDefault();
104
+ }
127
105
  }
128
- }
129
106
 
130
- if ( ! offset ) {
131
- return;
132
- }
107
+ if ( ! offset ) {
108
+ return;
109
+ }
133
110
 
134
- const activeElement = ( event.target as HTMLElement | null )
135
- ?.ownerDocument?.activeElement;
136
- if ( ! activeElement ) {
137
- return;
138
- }
111
+ const activeElement = ( event.target as HTMLElement | null )
112
+ ?.ownerDocument?.activeElement;
113
+ if ( ! activeElement ) {
114
+ return;
115
+ }
139
116
 
140
- const context = getFocusableContext( activeElement );
141
- if ( ! context ) {
142
- return;
143
- }
117
+ const context = getFocusableContext( activeElement );
118
+ if ( ! context ) {
119
+ return;
120
+ }
144
121
 
145
- const { index, focusables } = context;
146
- const nextIndex = cycle
147
- ? cycleValue( index, focusables.length, offset )
148
- : index + offset;
122
+ const { index, focusables } = context;
123
+ const nextIndex = cycle
124
+ ? cycleValue( index, focusables.length, offset )
125
+ : index + offset;
149
126
 
150
- if ( nextIndex >= 0 && nextIndex < focusables.length ) {
151
- focusables[ nextIndex ].focus();
152
- onNavigate( nextIndex, focusables[ nextIndex ] );
127
+ if ( nextIndex >= 0 && nextIndex < focusables.length ) {
128
+ focusables[ nextIndex ].focus();
129
+ onNavigate( nextIndex, focusables[ nextIndex ] as HTMLElement );
153
130
 
154
- // `preventDefault()` on tab to avoid having the browser move the focus
155
- // after this component has already moved it.
156
- if ( event.code === 'Tab' ) {
157
- event.preventDefault();
131
+ // `preventDefault()` on tab to avoid having the browser move the focus
132
+ // after this component has already moved it.
133
+ if ( event.code === 'Tab' ) {
134
+ event.preventDefault();
135
+ }
158
136
  }
159
137
  }
160
- }
161
138
 
162
- render() {
163
- const {
164
- children,
165
- stopNavigationEvents,
166
- eventToOffset,
167
- onNavigate,
168
- onKeyDown,
169
- cycle,
170
- onlyBrowserTabstops,
171
- forwardedRef,
172
- ...restProps
173
- } = this.props;
174
- return (
175
- <div ref={ this.bindContainer } { ...restProps }>
176
- { children }
177
- </div>
178
- );
179
- }
139
+ // We use DOM event listeners instead of React event listeners
140
+ // because we want to catch events from the underlying DOM tree.
141
+ // The React Tree can be different from the DOM tree when using
142
+ // portals. Block Toolbars for instance are rendered in a separate
143
+ // React Trees.
144
+ container.addEventListener( 'keydown', handleKeyDown );
145
+ return () => {
146
+ container.removeEventListener( 'keydown', handleKeyDown );
147
+ };
148
+ }, [
149
+ onKeyDown,
150
+ eventToOffset,
151
+ stopNavigationEvents,
152
+ cycle,
153
+ onNavigate,
154
+ getFocusableContext,
155
+ ] );
156
+
157
+ const mergedRef = useMergeRefs( [ containerRef, ref ] );
158
+
159
+ return (
160
+ <div ref={ mergedRef } { ...restProps }>
161
+ { children }
162
+ </div>
163
+ );
180
164
  }
181
165
 
182
- const forwardedNavigableContainer = (
183
- props: NavigableContainerProps,
184
- ref: ForwardedRef< HTMLDivElement >
185
- ) => {
186
- return <NavigableContainer { ...props } forwardedRef={ ref } />;
187
- };
188
- forwardedNavigableContainer.displayName = 'NavigableContainer';
166
+ const NavigableContainer = forwardRef( UnforwardedNavigableContainer );
167
+ NavigableContainer.displayName = 'NavigableContainer';
189
168
 
190
- export default forwardRef( forwardedNavigableContainer );
169
+ export default NavigableContainer;
@@ -215,6 +215,30 @@ describe( 'NavigableMenu', () => {
215
215
  expect( externalWrapperOnKeyDownSpy ).toHaveBeenCalledTimes( 2 );
216
216
  } );
217
217
 
218
+ it( 'should keep forwarded callback refs stable across rerenders', () => {
219
+ const refSpy = jest.fn();
220
+
221
+ const { rerender } = render(
222
+ <NavigableMenu ref={ refSpy }>
223
+ <button>Item 1</button>
224
+ </NavigableMenu>
225
+ );
226
+
227
+ expect( refSpy ).toHaveBeenCalledTimes( 1 );
228
+ expect( refSpy ).toHaveBeenCalledWith( expect.any( HTMLElement ) );
229
+
230
+ rerender(
231
+ <NavigableMenu ref={ refSpy }>
232
+ <button>Item 1</button>
233
+ </NavigableMenu>
234
+ );
235
+
236
+ // With a stable merged ref (useMergeRefs), the callback ref should
237
+ // not be called again on rerender. Previously, an inline ref callback
238
+ // would cause React to detach (null) and reattach on every render.
239
+ expect( refSpy ).toHaveBeenCalledTimes( 1 );
240
+ } );
241
+
218
242
  it( 'skips its internal logic when the tab key is pressed', async () => {
219
243
  const user = userEvent.setup();
220
244
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import type { ForwardedRef, ReactNode } from 'react';
4
+ import type { ReactNode } from 'react';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -35,10 +35,6 @@ export type NavigableContainerProps = WordPressComponentProps<
35
35
  * Gets an offset, given an event.
36
36
  */
37
37
  eventToOffset: ( event: KeyboardEvent ) => -1 | 0 | 1 | undefined;
38
- /**
39
- * The forwarded ref.
40
- */
41
- forwardedRef?: ForwardedRef< any >;
42
38
  /**
43
39
  * Whether to only consider browser tab stops.
44
40
  *
@@ -82,11 +82,12 @@ export const MoreExamplesStory: StoryFn< typeof Navigation > = ( {
82
82
  title="WordPress.org"
83
83
  />
84
84
  <NavigationItem item="item-5">
85
+ { /* eslint-disable-next-line react/jsx-no-target-blank */ }
85
86
  <a
86
87
  className="navigation-story__wordpress-icon"
87
88
  href="https://wordpress.org/"
88
89
  target="_blank"
89
- rel="noreferrer"
90
+ rel="noopener"
90
91
  >
91
92
  <Icon icon={ wordpress } />
92
93
  <em>Custom Content</em>
@@ -12,6 +12,7 @@ import { HStack } from '../../h-stack';
12
12
  import { Navigator, useNavigator } from '../';
13
13
 
14
14
  const meta: Meta< typeof Navigator > = {
15
+ tags: [ 'manifest' ],
15
16
  component: Navigator,
16
17
  subcomponents: {
17
18
  Screen: Navigator.Screen,
@@ -18,6 +18,7 @@ import NoticeList from '../list';
18
18
  import type { NoticeListProps } from '../types';
19
19
 
20
20
  const meta: Meta< typeof Notice > = {
21
+ tags: [ 'manifest' ],
21
22
  title: 'Components/Feedback/Notice',
22
23
  id: 'components-notice',
23
24
  component: Notice,
@@ -7,6 +7,7 @@ exports[`Notice should match snapshot 1`] = `
7
7
  >
8
8
  <div
9
9
  class="components-visually-hidden css-1ragr82-PolymorphicDiv emotion-0"
10
+ data-visually-hidden=""
10
11
  data-wp-c16t="true"
11
12
  data-wp-component="VisuallyHidden"
12
13
  style="border: 0px; clip: rect(1px, 1px, 1px, 1px); clip-path: inset( 50% ); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; word-wrap: normal; word-break: normal;"
@@ -25,7 +25,7 @@ const meta: Meta< typeof NumberControl > = {
25
25
  type: { control: { type: 'text' } },
26
26
  value: { control: false },
27
27
  },
28
- tags: [ 'status-experimental' ],
28
+ tags: [ 'status-experimental', 'manifest' ],
29
29
  parameters: {
30
30
  controls: { expanded: true },
31
31
  docs: { canvas: { sourceState: 'shown' } },
@@ -17,6 +17,7 @@ import PanelBody from '../body';
17
17
  import InputControl from '../../input-control';
18
18
 
19
19
  const meta: Meta< typeof Panel > = {
20
+ tags: [ 'manifest' ],
20
21
  title: 'Components/Containers/Panel',
21
22
  id: 'components-panel',
22
23
  component: Panel,
@@ -33,6 +33,7 @@ const AVAILABLE_PLACEMENTS: PopoverProps[ 'placement' ][] = [
33
33
  ];
34
34
 
35
35
  const meta: Meta< typeof Popover > = {
36
+ tags: [ 'manifest' ],
36
37
  title: 'Components/Overlays/Popover',
37
38
  id: 'components-popover',
38
39
  component: Popover,
@@ -9,6 +9,7 @@ import type { Meta, StoryFn } from '@storybook/react-vite';
9
9
  import { ProgressBar } from '..';
10
10
 
11
11
  const meta: Meta< typeof ProgressBar > = {
12
+ tags: [ 'manifest' ],
12
13
  component: ProgressBar,
13
14
  title: 'Components/Feedback/ProgressBar',
14
15
  id: 'components-progressbar',
@@ -14,6 +14,7 @@ import { useState } from '@wordpress/element';
14
14
  import RadioControl from '..';
15
15
 
16
16
  const meta: Meta< typeof RadioControl > = {
17
+ tags: [ 'manifest' ],
17
18
  component: RadioControl,
18
19
  title: 'Components/Selection & Input/Common/RadioControl',
19
20
  id: 'components-radiocontrol',
@@ -18,6 +18,7 @@ import RangeControl from '..';
18
18
  const ICONS = { starEmpty, starFilled, styles, wordpress };
19
19
 
20
20
  const meta: Meta< typeof RangeControl > = {
21
+ tags: [ 'manifest' ],
21
22
  component: RangeControl,
22
23
  title: 'Components/Selection & Input/Common/RangeControl',
23
24
  id: 'components-rangecontrol',
@@ -14,6 +14,7 @@ import { useState } from '@wordpress/element';
14
14
  import ResizableBox from '..';
15
15
 
16
16
  const meta: Meta< typeof ResizableBox > = {
17
+ tags: [ 'manifest' ],
17
18
  title: 'Components/Utilities/ResizableBox',
18
19
  id: 'components-resizablebox',
19
20
  component: ResizableBox,