@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.
- package/CHANGELOG.md +25 -0
- package/build/autocomplete/get-autocomplete-match.cjs +11 -2
- package/build/autocomplete/get-autocomplete-match.cjs.map +2 -2
- package/build/autocomplete/index.cjs +42 -11
- package/build/autocomplete/index.cjs.map +2 -2
- package/build/external-link/index.cjs +1 -1
- package/build/external-link/index.cjs.map +2 -2
- package/build/form-token-field/index.cjs +22 -6
- package/build/form-token-field/index.cjs.map +3 -3
- package/build/form-token-field/token-input.cjs +1 -1
- package/build/form-token-field/token-input.cjs.map +2 -2
- package/build/menu/popover.cjs +7 -3
- package/build/menu/popover.cjs.map +2 -2
- package/build/menu/styles.cjs +39 -16
- package/build/menu/styles.cjs.map +2 -2
- package/build/navigable-container/container.cjs +72 -110
- package/build/navigable-container/container.cjs.map +2 -2
- package/build/utils/breakpoint.cjs.map +1 -1
- package/build/utils/font.cjs.map +1 -1
- package/build/visually-hidden/component.cjs +1 -0
- package/build/visually-hidden/component.cjs.map +2 -2
- package/build-module/autocomplete/get-autocomplete-match.mjs +11 -2
- package/build-module/autocomplete/get-autocomplete-match.mjs.map +2 -2
- package/build-module/autocomplete/index.mjs +42 -11
- package/build-module/autocomplete/index.mjs.map +2 -2
- package/build-module/external-link/index.mjs +1 -1
- package/build-module/external-link/index.mjs.map +2 -2
- package/build-module/form-token-field/index.mjs +22 -6
- package/build-module/form-token-field/index.mjs.map +2 -2
- package/build-module/form-token-field/token-input.mjs +1 -1
- package/build-module/form-token-field/token-input.mjs.map +2 -2
- package/build-module/menu/popover.mjs +7 -3
- package/build-module/menu/popover.mjs.map +2 -2
- package/build-module/menu/styles.mjs +37 -16
- package/build-module/menu/styles.mjs.map +2 -2
- package/build-module/navigable-container/container.mjs +73 -111
- package/build-module/navigable-container/container.mjs.map +2 -2
- package/build-module/utils/breakpoint.mjs.map +1 -1
- package/build-module/utils/font.mjs.map +1 -1
- package/build-module/visually-hidden/component.mjs +1 -0
- package/build-module/visually-hidden/component.mjs.map +2 -2
- package/build-style/style-rtl.css +26 -2
- package/build-style/style.css +26 -2
- package/build-types/autocomplete/get-autocomplete-match.d.ts +10 -1
- package/build-types/autocomplete/get-autocomplete-match.d.ts.map +1 -1
- package/build-types/autocomplete/index.d.ts.map +1 -1
- package/build-types/base-control/stories/index.story.d.ts.map +1 -1
- package/build-types/button/stories/index.story.d.ts.map +1 -1
- package/build-types/card/stories/index.story.d.ts +0 -6
- package/build-types/card/stories/index.story.d.ts.map +1 -1
- package/build-types/checkbox-control/stories/index.story.d.ts.map +1 -1
- package/build-types/color-indicator/stories/index.story.d.ts.map +1 -1
- package/build-types/color-palette/stories/index.story.d.ts.map +1 -1
- package/build-types/color-picker/stories/index.story.d.ts.map +1 -1
- package/build-types/combobox-control/stories/index.story.d.ts.map +1 -1
- package/build-types/composite/stories/index.story.d.ts.map +1 -1
- package/build-types/custom-select-control/stories/index.story.d.ts.map +1 -1
- package/build-types/disabled/stories/index.story.d.ts.map +1 -1
- package/build-types/drop-zone/stories/index.story.d.ts.map +1 -1
- package/build-types/dropdown/stories/index.story.d.ts.map +1 -1
- package/build-types/external-link/index.d.ts.map +1 -1
- package/build-types/external-link/stories/index.story.d.ts.map +1 -1
- package/build-types/form-file-upload/stories/index.story.d.ts.map +1 -1
- package/build-types/form-toggle/stories/index.story.d.ts.map +1 -1
- package/build-types/form-token-field/index.d.ts.map +1 -1
- package/build-types/form-token-field/stories/index.story.d.ts.map +1 -1
- package/build-types/form-token-field/token-input.d.ts.map +1 -1
- package/build-types/form-token-field/types.d.ts +16 -2
- package/build-types/form-token-field/types.d.ts.map +1 -1
- package/build-types/gradient-picker/stories/index.story.d.ts.map +1 -1
- package/build-types/icon/stories/index.story.d.ts.map +1 -1
- package/build-types/keyboard-shortcuts/stories/index.story.d.ts.map +1 -1
- package/build-types/menu/popover.d.ts.map +1 -1
- package/build-types/menu/styles.d.ts +16 -1
- package/build-types/menu/styles.d.ts.map +1 -1
- package/build-types/menu-group/stories/index.story.d.ts.map +1 -1
- package/build-types/menu-item/stories/index.story.d.ts.map +1 -1
- package/build-types/menu-items-choice/stories/index.story.d.ts.map +1 -1
- package/build-types/modal/stories/index.story.d.ts.map +1 -1
- package/build-types/navigable-container/container.d.ts +3 -8
- package/build-types/navigable-container/container.d.ts.map +1 -1
- package/build-types/navigable-container/types.d.ts +1 -5
- package/build-types/navigable-container/types.d.ts.map +1 -1
- package/build-types/navigation/stories/utils/more-examples.d.ts.map +1 -1
- package/build-types/navigator/stories/index.story.d.ts.map +1 -1
- package/build-types/notice/stories/index.story.d.ts.map +1 -1
- package/build-types/panel/stories/index.story.d.ts.map +1 -1
- package/build-types/popover/stories/index.story.d.ts.map +1 -1
- package/build-types/progress-bar/stories/index.story.d.ts.map +1 -1
- package/build-types/radio-control/stories/index.story.d.ts.map +1 -1
- package/build-types/range-control/stories/index.story.d.ts.map +1 -1
- package/build-types/resizable-box/stories/index.story.d.ts.map +1 -1
- package/build-types/sandbox/stories/index.story.d.ts.map +1 -1
- package/build-types/scroll-lock/stories/index.story.d.ts.map +1 -1
- package/build-types/search-control/stories/index.story.d.ts.map +1 -1
- package/build-types/select-control/stories/index.story.d.ts.map +1 -1
- package/build-types/shortcut/stories/index.story.d.ts.map +1 -1
- package/build-types/slot-fill/stories/index.story.d.ts.map +1 -1
- package/build-types/snackbar/stories/index.story.d.ts.map +1 -1
- package/build-types/spinner/stories/index.story.d.ts.map +1 -1
- package/build-types/text-control/stories/index.story.d.ts.map +1 -1
- package/build-types/text-highlight/stories/index.story.d.ts.map +1 -1
- package/build-types/textarea-control/stories/index.story.d.ts.map +1 -1
- package/build-types/toggle-control/stories/index.story.d.ts.map +1 -1
- package/build-types/tooltip/stories/index.story.d.ts.map +1 -1
- package/build-types/tree-select/stories/index.story.d.ts.map +1 -1
- package/build-types/utils/breakpoint.d.ts +2 -1
- package/build-types/utils/breakpoint.d.ts.map +1 -1
- package/build-types/utils/font.d.ts +3 -2
- package/build-types/utils/font.d.ts.map +1 -1
- package/build-types/visually-hidden/component.d.ts.map +1 -1
- package/build-types/visually-hidden/stories/index.story.d.ts +0 -6
- package/build-types/visually-hidden/stories/index.story.d.ts.map +1 -1
- package/package.json +21 -21
- package/src/autocomplete/get-autocomplete-match.ts +25 -4
- package/src/autocomplete/index.tsx +69 -21
- package/src/autocomplete/test/get-autocomplete-match.ts +97 -75
- package/src/base-control/stories/index.story.tsx +1 -0
- package/src/button/stories/index.story.tsx +1 -0
- package/src/button-group/style.scss +1 -2
- package/src/card/stories/index.story.tsx +2 -9
- package/src/checkbox-control/stories/index.story.tsx +1 -0
- package/src/circular-option-picker/style.scss +8 -6
- package/src/color-indicator/stories/index.story.tsx +1 -0
- package/src/color-palette/stories/index.story.tsx +1 -0
- package/src/color-picker/stories/index.story.tsx +1 -0
- package/src/combobox-control/stories/index.story.tsx +1 -0
- package/src/composite/stories/index.story.tsx +1 -0
- package/src/confirm-dialog/stories/index.story.tsx +1 -1
- package/src/custom-select-control/stories/index.story.tsx +1 -0
- package/src/disabled/stories/index.story.tsx +1 -0
- package/src/drop-zone/stories/index.story.tsx +1 -0
- package/src/dropdown/stories/index.story.tsx +1 -0
- package/src/external-link/index.tsx +1 -6
- package/src/external-link/stories/index.story.tsx +2 -1
- package/src/external-link/style.scss +30 -2
- package/src/form-file-upload/stories/index.story.tsx +1 -0
- package/src/form-toggle/stories/index.story.tsx +1 -0
- package/src/form-toggle/style.scss +3 -2
- package/src/form-token-field/README.md +2 -1
- package/src/form-token-field/index.tsx +39 -9
- package/src/form-token-field/stories/index.story.tsx +2 -0
- package/src/form-token-field/test/index.tsx +70 -10
- package/src/form-token-field/token-input.tsx +1 -6
- package/src/form-token-field/types.ts +16 -2
- package/src/gradient-picker/stories/index.story.tsx +1 -0
- package/src/icon/stories/index.story.tsx +1 -0
- package/src/input-control/stories/index.story.tsx +1 -1
- package/src/item-group/stories/index.story.tsx +1 -1
- package/src/keyboard-shortcuts/stories/index.story.tsx +1 -0
- package/src/menu/popover.tsx +15 -8
- package/src/menu/styles.ts +26 -16
- package/src/menu/test/index.tsx +24 -34
- package/src/menu-group/stories/index.story.tsx +1 -0
- package/src/menu-item/stories/index.story.tsx +1 -0
- package/src/menu-items-choice/stories/index.story.tsx +1 -0
- package/src/mobile/link-settings/index.native.js +1 -1
- package/src/modal/stories/index.story.tsx +1 -0
- package/src/navigable-container/container.tsx +120 -141
- package/src/navigable-container/test/navigable-menu.tsx +24 -0
- package/src/navigable-container/types.ts +1 -5
- package/src/navigation/stories/utils/more-examples.tsx +2 -1
- package/src/navigator/stories/index.story.tsx +1 -0
- package/src/notice/stories/index.story.tsx +1 -0
- package/src/notice/test/__snapshots__/index.tsx.snap +1 -0
- package/src/number-control/stories/index.story.tsx +1 -1
- package/src/panel/stories/index.story.tsx +1 -0
- package/src/popover/stories/index.story.tsx +1 -0
- package/src/progress-bar/stories/index.story.tsx +1 -0
- package/src/radio-control/stories/index.story.tsx +1 -0
- package/src/range-control/stories/index.story.tsx +1 -0
- package/src/resizable-box/stories/index.story.tsx +1 -0
- package/src/resizable-box/style.scss +4 -5
- package/src/sandbox/stories/index.story.tsx +1 -0
- package/src/scroll-lock/stories/index.story.tsx +1 -0
- package/src/search-control/stories/index.story.tsx +1 -0
- package/src/select-control/stories/index.story.tsx +1 -0
- package/src/shortcut/stories/index.story.tsx +1 -0
- package/src/slot-fill/stories/index.story.tsx +1 -0
- package/src/snackbar/stories/index.story.tsx +1 -0
- package/src/spinner/stories/index.story.tsx +1 -0
- package/src/text-control/stories/index.story.tsx +1 -0
- package/src/text-highlight/stories/index.story.tsx +1 -0
- package/src/textarea-control/stories/index.story.tsx +1 -0
- package/src/toggle-control/stories/index.story.tsx +1 -0
- package/src/toggle-group-control/stories/index.story.tsx +1 -1
- package/src/toolbar/toolbar-group/index.tsx +2 -2
- package/src/tooltip/stories/index.story.tsx +1 -0
- package/src/tooltip/test/index.tsx +3 -2
- package/src/tree-grid/stories/index.story.tsx +1 -1
- package/src/tree-select/stories/index.story.tsx +1 -0
- package/src/truncate/stories/index.story.tsx +1 -1
- package/src/unit-control/stories/index.story.tsx +1 -1
- package/src/utils/breakpoint.js +1 -1
- package/src/utils/font.js +1 -1
- package/src/visually-hidden/component.tsx +1 -0
- package/src/visually-hidden/stories/index.story.tsx +2 -8
- package/build/card/context.cjs +0 -36
- package/build/card/context.cjs.map +0 -7
- package/build-module/card/context.mjs +0 -10
- package/build-module/card/context.mjs.map +0 -7
- package/build-types/card/context.d.ts +0 -3
- package/build-types/card/context.d.ts.map +0 -1
- package/build-types/visually-hidden/test/index.d.ts +0 -2
- package/build-types/visually-hidden/test/index.d.ts.map +0 -1
- package/src/card/context.ts +0 -9
- package/src/visually-hidden/test/__snapshots__/index.tsx.snap +0 -12
- package/src/visually-hidden/test/index.tsx +0 -17
package/src/menu/test/index.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ).
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
@@ -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',
|
|
@@ -6,7 +6,8 @@ import type { ForwardedRef } from 'react';
|
|
|
6
6
|
/**
|
|
7
7
|
* WordPress dependencies
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
}
|
|
60
|
+
const finder = onlyBrowserTabstops
|
|
61
|
+
? focus.tabbable
|
|
62
|
+
: focus.focusable;
|
|
63
|
+
const focusables = finder.find( containerRef.current );
|
|
74
64
|
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
107
|
+
if ( ! offset ) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
133
110
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
111
|
+
const activeElement = ( event.target as HTMLElement | null )
|
|
112
|
+
?.ownerDocument?.activeElement;
|
|
113
|
+
if ( ! activeElement ) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
139
116
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
117
|
+
const context = getFocusableContext( activeElement );
|
|
118
|
+
if ( ! context ) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
144
121
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
122
|
+
const { index, focusables } = context;
|
|
123
|
+
const nextIndex = cycle
|
|
124
|
+
? cycleValue( index, focusables.length, offset )
|
|
125
|
+
: index + offset;
|
|
149
126
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
127
|
+
if ( nextIndex >= 0 && nextIndex < focusables.length ) {
|
|
128
|
+
focusables[ nextIndex ].focus();
|
|
129
|
+
onNavigate( nextIndex, focusables[ nextIndex ] as HTMLElement );
|
|
153
130
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
183
|
-
|
|
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
|
|
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 {
|
|
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="
|
|
90
|
+
rel="noopener"
|
|
90
91
|
>
|
|
91
92
|
<Icon icon={ wordpress } />
|
|
92
93
|
<em>Custom Content</em>
|
|
@@ -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' } },
|
|
@@ -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,
|