-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
index.js
139 lines (121 loc) · 3.79 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
* WordPress dependencies
*/
import { NavigableMenu, Toolbar } from '@wordpress/components';
import {
useState,
useRef,
useLayoutEffect,
useEffect,
useCallback,
} from '@wordpress/element';
import deprecated from '@wordpress/deprecated';
import { focus } from '@wordpress/dom';
import { useShortcut } from '@wordpress/keyboard-shortcuts';
function useUpdateLayoutEffect( effect, deps ) {
const mounted = useRef( false );
useLayoutEffect( () => {
if ( mounted.current ) {
return effect();
}
mounted.current = true;
}, deps );
}
function hasOnlyToolbarItem( elements ) {
const dataProp = 'toolbarItem';
return ! elements.some( ( element ) => ! ( dataProp in element.dataset ) );
}
function focusFirstTabbableIn( container ) {
const [ firstTabbable ] = focus.tabbable.find( container );
if ( firstTabbable ) {
firstTabbable.focus();
}
}
function useIsAccessibleToolbar( ref ) {
/*
* By default, we'll assume the starting accessible state of the Toolbar
* is true, as it seems to be the most common case.
*
* Transitioning from an (initial) false to true state causes the
* <Toolbar /> component to mount twice, which is causing undesired
* side-effects. These side-effects appear to only affect certain
* E2E tests.
*
* This was initial discovered in this pull-request:
* https://github.com/WordPress/gutenberg/pull/23425
*/
const initialAccessibleToolbarState = true;
// By default, it's gonna render NavigableMenu. If all the tabbable elements
// inside the toolbar are ToolbarItem components (or derived components like
// ToolbarButton), then we can wrap them with the accessible Toolbar
// component.
const [ isAccessibleToolbar, setIsAccessibleToolbar ] = useState(
initialAccessibleToolbarState
);
const determineIsAccessibleToolbar = useCallback( () => {
const tabbables = focus.tabbable.find( ref.current );
const onlyToolbarItem = hasOnlyToolbarItem( tabbables );
if ( ! onlyToolbarItem ) {
deprecated( 'Using custom components as toolbar controls', {
alternative: 'ToolbarItem or ToolbarButton components',
link:
'https://developer.wordpress.org/block-editor/components/toolbar-button/#inside-blockcontrols',
} );
}
setIsAccessibleToolbar( onlyToolbarItem );
}, [] );
useLayoutEffect( determineIsAccessibleToolbar, [] );
useUpdateLayoutEffect( () => {
// Toolbar buttons may be rendered asynchronously, so we use
// MutationObserver to check if the toolbar subtree has been modified
const observer = new window.MutationObserver(
determineIsAccessibleToolbar
);
observer.observe( ref.current, { childList: true, subtree: true } );
return () => observer.disconnect();
}, [ isAccessibleToolbar ] );
return isAccessibleToolbar;
}
function useToolbarFocus( ref, focusOnMount, isAccessibleToolbar ) {
// Make sure we don't use modified versions of this prop
const [ initialFocusOnMount ] = useState( focusOnMount );
const focusToolbar = useCallback( () => {
focusFirstTabbableIn( ref.current );
}, [] );
useShortcut( 'core/block-editor/focus-toolbar', focusToolbar, {
bindGlobal: true,
eventName: 'keydown',
} );
useEffect( () => {
if ( initialFocusOnMount ) {
focusToolbar();
}
}, [ isAccessibleToolbar, initialFocusOnMount, focusToolbar ] );
}
function NavigableToolbar( { children, focusOnMount, ...props } ) {
const wrapper = useRef();
const isAccessibleToolbar = useIsAccessibleToolbar( wrapper );
useToolbarFocus( wrapper, focusOnMount, isAccessibleToolbar );
if ( isAccessibleToolbar ) {
return (
<Toolbar
label={ props[ 'aria-label' ] }
ref={ wrapper }
{ ...props }
>
{ children }
</Toolbar>
);
}
return (
<NavigableMenu
orientation="horizontal"
role="toolbar"
ref={ wrapper }
{ ...props }
>
{ children }
</NavigableMenu>
);
}
export default NavigableToolbar;