diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js
index 320f63555946c..c7632b0bc7168 100644
--- a/packages/block-editor/src/components/block-list/block.js
+++ b/packages/block-editor/src/components/block-list/block.js
@@ -322,7 +322,10 @@ function BlockListBlock( {
onShiftSelection();
event.preventDefault();
}
- } else {
+
+ // Avoid triggering multi-selection if we click toolbars/inspectors
+ // and all elements that are outside the Block Edit DOM tree.
+ } else if ( blockNodeRef.current.contains( event.target ) ) {
onSelectionStart( clientId );
// Allow user to escape out of a multi-selection to a singular
diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js
index 4eb26c2d50f86..83edd7814a0ed 100644
--- a/packages/block-editor/src/components/block-toolbar/index.js
+++ b/packages/block-editor/src/components/block-toolbar/index.js
@@ -31,8 +31,8 @@ function BlockToolbar( { blockClientIds, isValid, mode } ) {
{ mode === 'visual' && isValid && (
<>
-
-
+
+
>
) }
diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss
index 3d741214e9879..7d9f29c956e62 100644
--- a/packages/block-editor/src/components/block-toolbar/style.scss
+++ b/packages/block-editor/src/components/block-toolbar/style.scss
@@ -42,3 +42,7 @@
}
}
}
+
+.block-editor-block-toolbar__slot {
+ display: inline-flex;
+}
diff --git a/packages/block-editor/src/components/block-vertical-alignment-toolbar/index.js b/packages/block-editor/src/components/block-vertical-alignment-toolbar/index.js
index d3d94a0218b09..86414d5ddf07f 100644
--- a/packages/block-editor/src/components/block-vertical-alignment-toolbar/index.js
+++ b/packages/block-editor/src/components/block-vertical-alignment-toolbar/index.js
@@ -72,7 +72,7 @@ export default compose(
return {
isCollapsed: isCollapsed || ! isLargeViewport || (
! getSettings().hasFixedToolbar &&
- getBlockRootClientId( clientId )
+ !! getBlockRootClientId( clientId )
),
};
} ),
diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js
index b4fbbf7f3f241..21c6b5dd841e4 100644
--- a/packages/block-editor/src/components/navigable-toolbar/index.js
+++ b/packages/block-editor/src/components/navigable-toolbar/index.js
@@ -65,6 +65,17 @@ class NavigableToolbar extends Component {
if ( this.props.focusOnMount ) {
this.focusToolbar();
}
+
+ // We use DOM event listeners instead of React event listeners
+ // because we want to catch events from the underlying DOM tree
+ // The React Tree can be different from the DOM tree when using
+ // portals. Block Toolbars for instance are rendered in a separate
+ // React Tree.
+ this.toolbar.current.addEventListener( 'keydown', this.switchOnKeyDown );
+ }
+
+ componentwillUnmount() {
+ this.toolbar.current.removeEventListener( 'keydown', this.switchOnKeyDown );
}
render() {
@@ -74,7 +85,6 @@ class NavigableToolbar extends Component {
orientation="horizontal"
role="toolbar"
ref={ this.toolbar }
- onKeyDown={ this.switchOnKeyDown }
{ ...omit( props, [
'focusOnMount',
] ) }
diff --git a/packages/components/src/navigable-container/container.js b/packages/components/src/navigable-container/container.js
index 244d646de026f..be3a9d73be402 100644
--- a/packages/components/src/navigable-container/container.js
+++ b/packages/components/src/navigable-container/container.js
@@ -30,6 +30,21 @@ class NavigableContainer extends Component {
this.getFocusableIndex = this.getFocusableIndex.bind( this );
}
+ componentDidMount() {
+ // We use DOM event listeners instead of React event listeners
+ // because we want to catch events from the underlying DOM tree
+ // The React Tree can be different from the DOM tree when using
+ // portals. Block Toolbars for instance are rendered in a separate
+ // React Trees.
+ this.container.addEventListener( 'keydown', this.onKeyDown );
+ this.container.addEventListener( 'focus', this.onFocus );
+ }
+
+ componentWillUnmount() {
+ this.container.removeEventListener( 'keydown', this.onKeyDown );
+ this.container.removeEventListener( 'focus', this.onFocus );
+ }
+
bindContainer( ref ) {
const { forwardedRef } = this.props;
this.container = ref;
@@ -73,15 +88,13 @@ class NavigableContainer extends Component {
// eventToOffset returns undefined if the event is not handled by the component
if ( offset !== undefined && stopNavigationEvents ) {
// Prevents arrow key handlers bound to the document directly interfering
- event.nativeEvent.stopImmediatePropagation();
+ event.stopImmediatePropagation();
// When navigating a collection of items, prevent scroll containers
// from scrolling.
if ( event.target.getAttribute( 'role' ) === 'menuitem' ) {
event.preventDefault();
}
-
- event.stopPropagation();
}
if ( ! offset ) {
@@ -115,8 +128,7 @@ class NavigableContainer extends Component {
'onlyBrowserTabstops',
'forwardedRef',
] ) }
- onKeyDown={ this.onKeyDown }
- onFocus={ this.onFocus }>
+ >
{ children }
);
diff --git a/packages/components/src/navigable-container/test/menu.js b/packages/components/src/navigable-container/test/menu.js
index 7dff49f321a13..8f19afe270cb6 100644
--- a/packages/components/src/navigable-container/test/menu.js
+++ b/packages/components/src/navigable-container/test/menu.js
@@ -26,17 +26,14 @@ function fireKeyDown( container, keyCode, shiftKey ) {
stopped: false,
};
- container.simulate( 'keydown', {
- stopPropagation: () => {
- interaction.stopped = true;
- },
- preventDefault: () => {},
- nativeEvent: {
- stopImmediatePropagation: () => {},
- },
+ const event = new window.KeyboardEvent( 'keydown', {
keyCode,
shiftKey,
} );
+ event.stopImmediatePropagation = () => {
+ interaction.stopped = true;
+ };
+ container.getDOMNode().dispatchEvent( event );
return interaction;
}
diff --git a/packages/components/src/navigable-container/test/tabbable.js b/packages/components/src/navigable-container/test/tabbable.js
index b56421df41bb3..65bfbb387c2f3 100644
--- a/packages/components/src/navigable-container/test/tabbable.js
+++ b/packages/components/src/navigable-container/test/tabbable.js
@@ -26,17 +26,14 @@ function fireKeyDown( container, keyCode, shiftKey ) {
stopped: false,
};
- container.simulate( 'keydown', {
- stopPropagation: () => {
- interaction.stopped = true;
- },
- preventDefault: () => {},
- nativeEvent: {
- stopImmediatePropagation: () => {},
- },
+ const event = new window.KeyboardEvent( 'keydown', {
keyCode,
shiftKey,
} );
+ event.stopImmediatePropagation = () => {
+ interaction.stopped = true;
+ };
+ container.getDOMNode().dispatchEvent( event );
return interaction;
}
diff --git a/packages/components/src/slot-fill/README.md b/packages/components/src/slot-fill/README.md
index 23bbd7b77f941..20cd6d708bea6 100644
--- a/packages/components/src/slot-fill/README.md
+++ b/packages/components/src/slot-fill/README.md
@@ -70,6 +70,8 @@ Both `Slot` and `Fill` accept a `name` string prop, where a `Slot` with a given
- By default, events will bubble to their parents on the DOM hierarchy (native event bubbling)
- If `bubblesVirtually` is set to true, events will bubble to their virtual parent in the React elements hierarchy instead.
+ `Slot` with `bubblesVirtually` set to true also accept an optional `className` to add to the slot container.
+
`Slot` also accepts optional `children` function prop, which takes `fills` as a param. It allows to perform additional processing and wrap `fills` conditionally.
_Example_:
diff --git a/packages/components/src/slot-fill/context.js b/packages/components/src/slot-fill/context.js
index 7ee0d9806c400..587c1ccab5ccb 100644
--- a/packages/components/src/slot-fill/context.js
+++ b/packages/components/src/slot-fill/context.js
@@ -6,16 +6,18 @@ import { sortBy, forEach, without } from 'lodash';
/**
* WordPress dependencies
*/
-import { Component, createContext } from '@wordpress/element';
+import { Component, createContext, useContext, useState, useEffect } from '@wordpress/element';
-const { Provider, Consumer } = createContext( {
+const SlotFillContext = createContext( {
registerSlot: () => {},
unregisterSlot: () => {},
registerFill: () => {},
unregisterFill: () => {},
getSlot: () => {},
getFills: () => {},
+ subscribe: () => {},
} );
+const { Provider, Consumer } = SlotFillContext;
class SlotFillProvider extends Component {
constructor() {
@@ -27,23 +29,26 @@ class SlotFillProvider extends Component {
this.unregisterFill = this.unregisterFill.bind( this );
this.getSlot = this.getSlot.bind( this );
this.getFills = this.getFills.bind( this );
+ this.subscribe = this.subscribe.bind( this );
this.slots = {};
this.fills = {};
- this.state = {
+ this.listeners = [];
+ this.contextValue = {
registerSlot: this.registerSlot,
unregisterSlot: this.unregisterSlot,
registerFill: this.registerFill,
unregisterFill: this.unregisterFill,
getSlot: this.getSlot,
getFills: this.getFills,
+ subscribe: this.subscribe,
};
}
registerSlot( name, slot ) {
const previousSlot = this.slots[ name ];
this.slots[ name ] = slot;
- this.forceUpdateFills( name );
+ this.triggerListeners();
// Sometimes the fills are registered after the initial render of slot
// But before the registerSlot call, we need to rerender the slot
@@ -75,7 +80,7 @@ class SlotFillProvider extends Component {
}
delete this.slots[ name ];
- this.forceUpdateFills( name );
+ this.triggerListeners();
}
unregisterFill( name, instance ) {
@@ -106,12 +111,6 @@ class SlotFillProvider extends Component {
} );
}
- forceUpdateFills( name ) {
- forEach( this.fills[ name ], ( instance ) => {
- instance.forceUpdate();
- } );
- }
-
forceUpdateSlot( name ) {
const slot = this.getSlot( name );
@@ -120,14 +119,48 @@ class SlotFillProvider extends Component {
}
}
+ triggerListeners() {
+ this.listeners.forEach( ( listener ) => listener() );
+ }
+
+ subscribe( listener ) {
+ this.listeners.push( listener );
+
+ return () => {
+ this.listeners = without( this.listeners, listener );
+ };
+ }
+
render() {
return (
-
+
{ this.props.children }
);
}
}
+/**
+ * React hook returning the active slot given a name.
+ *
+ * @param {string} name Slot name.
+ * @return {Object} Slot object.
+ */
+export const useSlot = ( name ) => {
+ const { getSlot, subscribe } = useContext( SlotFillContext );
+ const [ slot, setSlot ] = useState( getSlot( name ) );
+
+ useEffect( () => {
+ setSlot( getSlot( name ) );
+ const unsubscribe = subscribe( () => {
+ setSlot( getSlot( name ) );
+ } );
+
+ return unsubscribe;
+ }, [ name ] );
+
+ return slot;
+};
+
export default SlotFillProvider;
export { Consumer };
diff --git a/packages/components/src/slot-fill/fill.js b/packages/components/src/slot-fill/fill.js
index a0c2876643f04..3600e177e5434 100644
--- a/packages/components/src/slot-fill/fill.js
+++ b/packages/components/src/slot-fill/fill.js
@@ -6,19 +6,17 @@ import { isFunction } from 'lodash';
/**
* WordPress dependencies
*/
-import { createPortal, useLayoutEffect, useRef, useState } from '@wordpress/element';
+import { createPortal, useLayoutEffect, useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
-import { Consumer } from './context';
+import { Consumer, useSlot } from './context';
let occurrences = 0;
-function FillComponent( { name, getSlot, children, registerFill, unregisterFill } ) {
- // Random state used to rerender the component if needed, ideally we don't need this
- const [ , updateRerenderState ] = useState( {} );
- const rerender = () => updateRerenderState( {} );
+function FillComponent( { name, children, registerFill, unregisterFill } ) {
+ const slot = useSlot( name );
const ref = useRef( {
name,
@@ -30,14 +28,12 @@ function FillComponent( { name, getSlot, children, registerFill, unregisterFill
}
useLayoutEffect( () => {
- ref.current.forceUpdate = rerender;
registerFill( name, ref.current );
return () => unregisterFill( name, ref.current );
}, [] );
useLayoutEffect( () => {
ref.current.children = children;
- const slot = getSlot( name );
if ( slot && ! slot.props.bubblesVirtually ) {
slot.forceUpdate();
}
@@ -53,8 +49,6 @@ function FillComponent( { name, getSlot, children, registerFill, unregisterFill
registerFill( name, ref.current );
}, [ name ] );
- const slot = getSlot( name );
-
if ( ! slot || ! slot.node || ! slot.props.bubblesVirtually ) {
return null;
}
@@ -69,10 +63,9 @@ function FillComponent( { name, getSlot, children, registerFill, unregisterFill
const Fill = ( props ) => (
- { ( { getSlot, registerFill, unregisterFill } ) => (
+ { ( { registerFill, unregisterFill } ) => (
diff --git a/packages/components/src/slot-fill/slot.js b/packages/components/src/slot-fill/slot.js
index 6de69f74f865a..4578656b5efaa 100644
--- a/packages/components/src/slot-fill/slot.js
+++ b/packages/components/src/slot-fill/slot.js
@@ -56,10 +56,10 @@ class SlotComponent extends Component {
}
render() {
- const { children, name, bubblesVirtually = false, fillProps = {}, getFills } = this.props;
+ const { children, name, bubblesVirtually = false, fillProps = {}, getFills, className } = this.props;
if ( bubblesVirtually ) {
- return ;
+ return ;
}
const fills = map( getFills( name, this ), ( fill ) => {
diff --git a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js
index 3f0e1c8c3f55c..378180b58cd82 100644
--- a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js
+++ b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js
@@ -23,7 +23,7 @@ describe( 'Navigating the block hierarchy', () => {
await page.click( '[aria-label="Two columns; equal split"]' );
// Add a paragraph in the first column.
- await pressKeyTimes( 'Tab', 5 ); // Tab to inserter.
+ await pressKeyTimes( 'Tab', 3 ); // Tab to inserter.
await page.keyboard.press( 'Enter' ); // Activate inserter.
await page.keyboard.type( 'Paragraph' );
await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result.
@@ -50,7 +50,7 @@ describe( 'Navigating the block hierarchy', () => {
await lastColumnsBlockMenuItem.click();
// Insert text in the last column block.
- await pressKeyTimes( 'Tab', 5 ); // Tab to inserter.
+ await pressKeyTimes( 'Tab', 3 ); // Tab to inserter.
await page.keyboard.press( 'Enter' ); // Activate inserter.
await page.keyboard.type( 'Paragraph' );
await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result.
@@ -65,7 +65,7 @@ describe( 'Navigating the block hierarchy', () => {
await page.click( '[aria-label="Two columns; equal split"]' );
// Add a paragraph in the first column.
- await pressKeyTimes( 'Tab', 5 ); // Tab to inserter.
+ await pressKeyTimes( 'Tab', 3 ); // Tab to inserter.
await page.keyboard.press( 'Enter' ); // Activate inserter.
await page.keyboard.type( 'Paragraph' );
await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result.
@@ -92,7 +92,7 @@ describe( 'Navigating the block hierarchy', () => {
await page.keyboard.press( 'Enter' );
// Insert text in the last column block
- await pressKeyTimes( 'Tab', 5 ); // Tab to inserter.
+ await pressKeyTimes( 'Tab', 3 ); // Tab to inserter.
await page.keyboard.press( 'Enter' ); // Activate inserter.
await page.keyboard.type( 'Paragraph' );
await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result.