diff --git a/package-lock.json b/package-lock.json
index d90064968229a8..eb9f9fb408f639 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19788,6 +19788,7 @@
"requires": {
"@babel/runtime": "^7.16.0",
"@wordpress/api-fetch": "file:packages/api-fetch",
+ "@wordpress/block-editor": "file:packages/block-editor",
"@wordpress/blocks": "file:packages/blocks",
"@wordpress/components": "file:packages/components",
"@wordpress/compose": "file:packages/compose",
diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md
index 94a227890243b6..207dd8788d963d 100644
--- a/packages/block-editor/README.md
+++ b/packages/block-editor/README.md
@@ -518,6 +518,19 @@ _Related_
Undocumented declaration.
+### Placeholder
+
+Placeholder for use in blocks. Creates an admin styling context and a tabbing
+context in the block editor's writing flow.
+
+_Parameters_
+
+- _props_ `Object`:
+
+_Returns_
+
+- `WPComponent`: The component
+
### PlainText
_Related_
diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js
index a3e522b080af8b..7095bd7e2dbc87 100644
--- a/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js
+++ b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js
@@ -62,6 +62,7 @@ function useInitialPosition( clientId ) {
export function useFocusFirstElement( clientId ) {
const ref = useRef();
const initialPosition = useInitialPosition( clientId );
+ const isMounting = useRef( true );
useEffect( () => {
if ( initialPosition === undefined || initialPosition === null ) {
@@ -79,16 +80,25 @@ export function useFocusFirstElement( clientId ) {
return;
}
- // Find all tabbables within node.
- const textInputs = focus.tabbable
- .find( ref.current )
- .filter( ( node ) => isTextField( node ) );
+ let target = ref.current;
// If reversed (e.g. merge via backspace), use the last in the set of
// tabbables.
const isReverse = -1 === initialPosition;
- const target =
- ( isReverse ? last : first )( textInputs ) || ref.current;
+
+ // Find all text fields or placeholders within the block.
+ const candidates = focus.tabbable
+ .find( target )
+ .filter(
+ ( node ) =>
+ isTextField( node ) ||
+ ( isMounting.current &&
+ node.classList.contains(
+ 'wp-block-editor-placeholder'
+ ) )
+ );
+
+ target = ( isReverse ? last : first )( candidates ) || target;
if ( ! isInsideRootBlock( ref.current, target ) ) {
ref.current.focus();
@@ -98,5 +108,9 @@ export function useFocusFirstElement( clientId ) {
placeCaretAtHorizontalEdge( target, isReverse );
}, [ initialPosition ] );
+ useEffect( () => {
+ isMounting.current = false;
+ }, [] );
+
return ref;
}
diff --git a/packages/block-editor/src/components/block-variation-picker/index.js b/packages/block-editor/src/components/block-variation-picker/index.js
index 24bdfa272c34b2..262f5e22b3e49b 100644
--- a/packages/block-editor/src/components/block-variation-picker/index.js
+++ b/packages/block-editor/src/components/block-variation-picker/index.js
@@ -7,9 +7,14 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { Button, Placeholder } from '@wordpress/components';
+import { Button } from '@wordpress/components';
import { layout } from '@wordpress/icons';
+/**
+ * Internal dependencies
+ */
+import Placeholder from '../placeholder';
+
function BlockVariationPicker( {
icon = layout,
label = __( 'Choose variation' ),
diff --git a/packages/block-editor/src/components/embedded-admin-context/index.js b/packages/block-editor/src/components/embedded-admin-context/index.js
new file mode 100644
index 00000000000000..3db90e1b45c72f
--- /dev/null
+++ b/packages/block-editor/src/components/embedded-admin-context/index.js
@@ -0,0 +1,157 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ useRefEffect,
+ useConstrainedTabbing,
+ useMergeRefs,
+} from '@wordpress/compose';
+import { useState, createPortal } from '@wordpress/element';
+import { ENTER, SPACE, ESCAPE } from '@wordpress/keycodes';
+import { focus } from '@wordpress/dom';
+import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components';
+
+/**
+ * Embeds the given children in shadow DOM that has the same styling as the top
+ * window (admin). A button is returned to allow the keyboard user to enter this
+ * context. Visually, it appears inline, but it is styled as the admin, not as
+ * the editor content.
+ *
+ * @param {Object} props Button props.
+ *
+ * @return {WPComponent} A button to enter the embedded admin context.
+ */
+export default function EmbeddedAdminContext( props ) {
+ const [ shadow, setShadow ] = useState();
+ const [ hasFocus, setHasFocus ] = useState();
+ const ref = useRefEffect( ( element ) => {
+ const root = element.attachShadow( { mode: 'open' } );
+
+ // Copy all admin styles to the shadow DOM.
+ const style = document.createElement( 'style' );
+ Array.from( document.styleSheets ).forEach( ( styleSheet ) => {
+ // Technically, it's fine to include this, but these are styles that
+ // target other components, so there's performance gain in not
+ // including them. Below, we use `StyleProvider` to render emotion
+ // styles in shadow DOM.
+ if ( styleSheet.ownerNode.getAttribute( 'data-emotion' ) ) {
+ return;
+ }
+
+ // Try to avoid requests for stylesheets of which we already
+ // know the CSS rules.
+ try {
+ let cssText = '';
+
+ for ( const cssRule of styleSheet.cssRules ) {
+ cssText += cssRule.cssText;
+ }
+
+ style.textContent += cssText;
+ } catch ( e ) {
+ root.appendChild( styleSheet.ownerNode.cloneNode( true ) );
+ }
+ } );
+ root.appendChild( style );
+ setShadow( root );
+
+ function onFocusIn() {
+ setHasFocus( true );
+ }
+
+ function onFocusOut() {
+ setHasFocus( false );
+ }
+
+ /**
+ * When pressing ENTER or SPACE on the wrapper (button), focus the first
+ * tabbable inside the shadow DOM.
+ *
+ * @param {KeyboardEvent} event The keyboard event.
+ */
+ function onKeyDown( event ) {
+ if ( element !== event.path[ 0 ] ) return;
+ if ( event.keyCode !== ENTER && event.keyCode !== SPACE ) return;
+
+ event.preventDefault();
+
+ const [ firstTabbable ] = focus.tabbable.find( root );
+ if ( firstTabbable ) firstTabbable.focus();
+ }
+
+ /**
+ * When pressing ESCAPE inside the shadow DOM, focus the wrapper
+ * (button).
+ *
+ * @param {KeyboardEvent} event The keyboard event.
+ */
+ function onRootKeyDown( event ) {
+ if ( event.keyCode !== ESCAPE ) return;
+
+ root.host.focus();
+ event.preventDefault();
+ }
+
+ let timeoutId;
+
+ /**
+ * When clicking inside the shadow DOM, temporarily remove the ability
+ * to catch focus, so focus moves to a focusable parent.
+ * This is done so that when the user clicks inside a placeholder, the
+ * block receives focus, which can handle delete, enter, etc.
+ */
+ function onMouseDown() {
+ element.removeAttribute( 'tabindex' );
+ timeoutId = setTimeout( () =>
+ element.setAttribute( 'tabindex', '0' )
+ );
+ }
+
+ root.addEventListener( 'focusin', onFocusIn );
+ root.addEventListener( 'focusout', onFocusOut );
+ root.addEventListener( 'keydown', onRootKeyDown );
+ element.addEventListener( 'keydown', onKeyDown );
+ element.addEventListener( 'mousedown', onMouseDown );
+ return () => {
+ root.removeEventListener( 'focusin', onFocusIn );
+ root.removeEventListener( 'focusout', onFocusOut );
+ root.removeEventListener( 'keydown', onRootKeyDown );
+ element.removeEventListener( 'keydown', onKeyDown );
+ element.removeEventListener( 'mousedown', onMouseDown );
+ clearTimeout( timeoutId );
+ };
+ }, [] );
+
+ const dialogRef = useRefEffect( ( element ) => {
+ if (
+ element.getRootNode().host !== element.ownerDocument.activeElement
+ )
+ return;
+
+ const [ firstTabbable ] = focus.tabbable.find( element );
+ if ( firstTabbable ) firstTabbable.focus();
+ }, [] );
+
+ const content = (
+
+
+ { props.children }
+
+
+ );
+
+ return (
+
+ { shadow && createPortal( content, shadow ) }
+
+ );
+}
diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js
index b1f991ce5c4d50..5deb4fb7483eed 100644
--- a/packages/block-editor/src/components/index.js
+++ b/packages/block-editor/src/components/index.js
@@ -71,6 +71,7 @@ export { default as __experimentalLinkControlSearchItem } from './link-control/s
export { default as LineHeightControl } from './line-height-control';
export { default as __experimentalListView } from './list-view';
export { default as MediaReplaceFlow } from './media-replace-flow';
+export { default as Placeholder } from './placeholder';
export { default as MediaPlaceholder } from './media-placeholder';
export { default as MediaUpload } from './media-upload';
export { default as MediaUploadCheck } from './media-upload/check';
diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js
index f13206870f815e..df321e0778feee 100644
--- a/packages/block-editor/src/components/media-placeholder/index.js
+++ b/packages/block-editor/src/components/media-placeholder/index.js
@@ -10,7 +10,6 @@ import classnames from 'classnames';
import {
Button,
FormFileUpload,
- Placeholder,
DropZone,
withFilters,
} from '@wordpress/components';
@@ -23,6 +22,7 @@ import { keyboardReturn } from '@wordpress/icons';
/**
* Internal dependencies
*/
+import Placeholder from '../placeholder';
import MediaUpload from '../media-upload';
import MediaUploadCheck from '../media-upload/check';
import URLPopover from '../url-popover';
diff --git a/packages/block-editor/src/components/placeholder/index.js b/packages/block-editor/src/components/placeholder/index.js
new file mode 100644
index 00000000000000..da0f1a24c0f10b
--- /dev/null
+++ b/packages/block-editor/src/components/placeholder/index.js
@@ -0,0 +1,33 @@
+/**
+ * WordPress dependencies
+ */
+import { Placeholder } from '@wordpress/components';
+import { __, sprintf } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import EmbeddedAdminContext from '../embedded-admin-context';
+
+/**
+ * Placeholder for use in blocks. Creates an admin styling context and a tabbing
+ * context in the block editor's writing flow.
+ *
+ * @param {Object} props
+ *
+ * @return {WPComponent} The component
+ */
+export default function IsolatedPlaceholder( props ) {
+ return (
+
+
+
+ );
+}
diff --git a/packages/block-editor/src/components/writing-flow/use-tab-nav.js b/packages/block-editor/src/components/writing-flow/use-tab-nav.js
index 7f61046809d71a..8ceb5e0375d47c 100644
--- a/packages/block-editor/src/components/writing-flow/use-tab-nav.js
+++ b/packages/block-editor/src/components/writing-flow/use-tab-nav.js
@@ -12,16 +12,6 @@ import { useRef } from '@wordpress/element';
*/
import { store as blockEditorStore } from '../../store';
-function isFormElement( element ) {
- const { tagName } = element;
- return (
- tagName === 'INPUT' ||
- tagName === 'BUTTON' ||
- tagName === 'SELECT' ||
- tagName === 'TEXTAREA'
- );
-}
-
export default function useTabNav() {
const container = useRef();
const focusCaptureBeforeRef = useRef();
@@ -104,8 +94,13 @@ export default function useTabNav() {
return;
}
+ if (
+ event.target.classList.contains( 'wp-block-editor-placeholder' )
+ ) {
+ return;
+ }
+
const isShift = event.shiftKey;
- const direction = isShift ? 'findPrevious' : 'findNext';
if ( ! hasMultiSelection() && ! getSelectedBlockClientId() ) {
// Preserve the behaviour of entering navigation mode when
@@ -118,18 +113,6 @@ export default function useTabNav() {
return;
}
- // Allow tabbing between form elements rendered in a block,
- // such as inside a placeholder. Form elements are generally
- // meant to be UI rather than part of the content. Ideally
- // these are not rendered in the content and perhaps in the
- // future they can be rendered in an iframe or shadow DOM.
- if (
- isFormElement( event.target ) &&
- isFormElement( focus.tabbable[ direction ]( event.target ) )
- ) {
- return;
- }
-
const next = isShift ? focusCaptureBeforeRef : focusCaptureAfterRef;
// Disable focus capturing on the focus capture element, so it
diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js
index ce2bb4c89b90ab..a6f671a0248e93 100644
--- a/packages/block-library/src/block/edit.js
+++ b/packages/block-library/src/block/edit.js
@@ -8,7 +8,6 @@ import {
store as coreStore,
} from '@wordpress/core-data';
import {
- Placeholder,
Spinner,
ToolbarGroup,
ToolbarButton,
@@ -25,6 +24,7 @@ import {
InspectorControls,
useBlockProps,
Warning,
+ Placeholder,
} from '@wordpress/block-editor';
import { store as reusableBlocksStore } from '@wordpress/reusable-blocks';
import { ungroup } from '@wordpress/icons';
diff --git a/packages/block-library/src/calendar/edit.js b/packages/block-library/src/calendar/edit.js
index 5ee7fe14ed072d..cb3137b6ee9a3b 100644
--- a/packages/block-library/src/calendar/edit.js
+++ b/packages/block-library/src/calendar/edit.js
@@ -8,10 +8,10 @@ import memoize from 'memize';
* WordPress dependencies
*/
import { calendar as icon } from '@wordpress/icons';
-import { Disabled, Placeholder, Spinner } from '@wordpress/components';
+import { Disabled, Spinner } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import ServerSideRender from '@wordpress/server-side-render';
-import { useBlockProps } from '@wordpress/block-editor';
+import { useBlockProps, Placeholder } from '@wordpress/block-editor';
import { store as coreStore } from '@wordpress/core-data';
import { __ } from '@wordpress/i18n';
diff --git a/packages/block-library/src/categories/edit.js b/packages/block-library/src/categories/edit.js
index 08276c026b3023..78dcc9b2da3a37 100644
--- a/packages/block-library/src/categories/edit.js
+++ b/packages/block-library/src/categories/edit.js
@@ -8,14 +8,17 @@ import { times, unescape } from 'lodash';
*/
import {
PanelBody,
- Placeholder,
Spinner,
ToggleControl,
VisuallyHidden,
} from '@wordpress/components';
import { useInstanceId } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
-import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
+import {
+ InspectorControls,
+ useBlockProps,
+ Placeholder,
+} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import { pin } from '@wordpress/icons';
import { store as coreStore } from '@wordpress/core-data';
diff --git a/packages/block-library/src/embed/embed-placeholder.js b/packages/block-library/src/embed/embed-placeholder.js
index e3ffb5ac02845c..858033811ab5d5 100644
--- a/packages/block-library/src/embed/embed-placeholder.js
+++ b/packages/block-library/src/embed/embed-placeholder.js
@@ -2,8 +2,8 @@
* WordPress dependencies
*/
import { __, _x } from '@wordpress/i18n';
-import { Button, Placeholder, ExternalLink } from '@wordpress/components';
-import { BlockIcon } from '@wordpress/block-editor';
+import { Button, ExternalLink } from '@wordpress/components';
+import { BlockIcon, Placeholder } from '@wordpress/block-editor';
const EmbedPlaceholder = ( {
icon,
diff --git a/packages/block-library/src/embed/embed-preview.js b/packages/block-library/src/embed/embed-preview.js
index 9e04efe78a0bfe..15116f9607539f 100644
--- a/packages/block-library/src/embed/embed-preview.js
+++ b/packages/block-library/src/embed/embed-preview.js
@@ -12,8 +12,8 @@ import classnames from 'classnames/dedupe';
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
-import { Placeholder, SandBox } from '@wordpress/components';
-import { RichText, BlockIcon } from '@wordpress/block-editor';
+import { SandBox } from '@wordpress/components';
+import { RichText, BlockIcon, Placeholder } from '@wordpress/block-editor';
import { Component } from '@wordpress/element';
import { createBlock } from '@wordpress/blocks';
diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js
index 0f8d41b758957a..e10504282cab79 100644
--- a/packages/block-library/src/latest-posts/edit.js
+++ b/packages/block-library/src/latest-posts/edit.js
@@ -11,7 +11,6 @@ import { RawHTML } from '@wordpress/element';
import {
BaseControl,
PanelBody,
- Placeholder,
QueryControls,
RadioControl,
RangeControl,
@@ -28,6 +27,7 @@ import {
__experimentalImageSizeControl as ImageSizeControl,
useBlockProps,
store as blockEditorStore,
+ Placeholder,
} from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { pin, list, grid } from '@wordpress/icons';
diff --git a/packages/block-library/src/post-comment/edit.js b/packages/block-library/src/post-comment/edit.js
index e78ecd47bab160..a68ed2c0eda12d 100644
--- a/packages/block-library/src/post-comment/edit.js
+++ b/packages/block-library/src/post-comment/edit.js
@@ -2,10 +2,14 @@
* WordPress dependencies
*/
import { __, _x } from '@wordpress/i18n';
-import { Placeholder, TextControl, Button } from '@wordpress/components';
+import { TextControl, Button } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { blockDefault } from '@wordpress/icons';
-import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
+import {
+ useBlockProps,
+ useInnerBlocksProps,
+ Placeholder,
+} from '@wordpress/block-editor';
const ALLOWED_BLOCKS = [
'core/comment-author-avatar',
diff --git a/packages/block-library/src/rss/edit.js b/packages/block-library/src/rss/edit.js
index 46a3e6a1a105d3..ad6ddb23361a3f 100644
--- a/packages/block-library/src/rss/edit.js
+++ b/packages/block-library/src/rss/edit.js
@@ -5,12 +5,12 @@ import {
BlockControls,
InspectorControls,
useBlockProps,
+ Placeholder,
} from '@wordpress/block-editor';
import {
Button,
Disabled,
PanelBody,
- Placeholder,
RangeControl,
TextControl,
ToggleControl,
diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js
index 6719a4244ab8b0..7f26a6c4caab53 100644
--- a/packages/block-library/src/site-logo/edit.js
+++ b/packages/block-library/src/site-logo/edit.js
@@ -18,8 +18,8 @@ import {
Spinner,
ToggleControl,
ToolbarButton,
- Placeholder,
Button,
+ Placeholder,
} from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import {
diff --git a/packages/block-library/src/table-of-contents/edit.js b/packages/block-library/src/table-of-contents/edit.js
index 44cd67303c7456..796d775ff16cf0 100644
--- a/packages/block-library/src/table-of-contents/edit.js
+++ b/packages/block-library/src/table-of-contents/edit.js
@@ -12,11 +12,11 @@ import {
InspectorControls,
store as blockEditorStore,
useBlockProps,
+ Placeholder,
} from '@wordpress/block-editor';
import { createBlock, store as blocksStore } from '@wordpress/blocks';
import {
PanelBody,
- Placeholder,
ToggleControl,
ToolbarButton,
ToolbarGroup,
diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js
index 113dd178b3caf1..5989b857237df4 100644
--- a/packages/block-library/src/table/edit.js
+++ b/packages/block-library/src/table/edit.js
@@ -16,12 +16,12 @@ import {
useBlockProps,
__experimentalUseColorProps as useColorProps,
__experimentalUseBorderProps as useBorderProps,
+ Placeholder,
} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import {
Button,
PanelBody,
- Placeholder,
TextControl,
ToggleControl,
ToolbarDropdownMenu,
diff --git a/packages/block-library/src/template-part/edit/placeholder/index.js b/packages/block-library/src/template-part/edit/placeholder/index.js
index 64a730535c0f5b..91a186061fae03 100644
--- a/packages/block-library/src/template-part/edit/placeholder/index.js
+++ b/packages/block-library/src/template-part/edit/placeholder/index.js
@@ -9,9 +9,10 @@ import { find, kebabCase } from 'lodash';
import { __, sprintf } from '@wordpress/i18n';
import { useCallback, useState } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
-import { Placeholder, Dropdown, Button, Spinner } from '@wordpress/components';
+import { Dropdown, Button, Spinner } from '@wordpress/components';
import { serialize } from '@wordpress/blocks';
import { store as coreStore } from '@wordpress/core-data';
+import { Placeholder } from '@wordpress/block-editor';
/**
* Internal dependencies
diff --git a/packages/components/src/placeholder/test/index.js b/packages/components/src/placeholder/test/index.js
index 758c6bed303942..69fba05049cb29 100644
--- a/packages/components/src/placeholder/test/index.js
+++ b/packages/components/src/placeholder/test/index.js
@@ -12,7 +12,7 @@ import { useResizeObserver } from '@wordpress/compose';
/**
* Internal dependencies
*/
-import Placeholder from '../';
+import { Placeholder } from '../';
describe( 'Placeholder', () => {
beforeEach( () => {
diff --git a/packages/dom/src/focusable.js b/packages/dom/src/focusable.js
index 75e4f72f0c1f77..b3c6ce8ef7a417 100644
--- a/packages/dom/src/focusable.js
+++ b/packages/dom/src/focusable.js
@@ -86,14 +86,14 @@ function isValidFocusableArea( element ) {
/**
* Returns all focusable elements within a given context.
*
- * @param {Element} context Element in which to search.
- * @param {Object} [options]
- * @param {boolean} [options.sequential] If set, only return elements that are
- * sequentially focusable.
- * Non-interactive elements with a
- * negative `tabindex` are focusable but
- * not sequentially focusable.
- * https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute
+ * @param {Element|Document|ShadowRoot} context Element in which to search.
+ * @param {Object} [options]
+ * @param {boolean} [options.sequential] If set, only return elements that are
+ * sequentially focusable.
+ * Non-interactive elements with a
+ * negative `tabindex` are focusable but
+ * not sequentially focusable.
+ * https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute
*
* @return {Element[]} Focusable elements.
*/
diff --git a/packages/dom/src/tabbable.js b/packages/dom/src/tabbable.js
index 64cef7f2a24856..5e5200a0f1758c 100644
--- a/packages/dom/src/tabbable.js
+++ b/packages/dom/src/tabbable.js
@@ -148,7 +148,8 @@ function filterTabbable( focusables ) {
}
/**
- * @param {Element} context
+ * @param {Element|Document|ShadowRoot} context
+ *
* @return {Element[]} Tabbable elements within the context.
*/
export function find( context ) {
@@ -162,7 +163,9 @@ export function find( context ) {
* to the active element.
*/
export function findPrevious( element ) {
- const focusables = findFocusable( element.ownerDocument.body );
+ const focusables = findFocusable(
+ /** @type {Document|ShadowRoot} */ ( element.getRootNode() )
+ );
const index = focusables.indexOf( element );
// Remove all focusables after and including `element`.
@@ -178,7 +181,9 @@ export function findPrevious( element ) {
* to the active element.
*/
export function findNext( element ) {
- const focusables = findFocusable( element.ownerDocument.body );
+ const focusables = findFocusable(
+ /** @type {Document|ShadowRoot} */ ( element.getRootNode() )
+ );
const index = focusables.indexOf( element );
// Remove all focusables before and including `element`.
diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md
index 210c15c101433c..e83bda92d5e2bc 100644
--- a/packages/e2e-test-utils/README.md
+++ b/packages/e2e-test-utils/README.md
@@ -107,6 +107,14 @@ _Parameters_
- _buttonLabel_ `string`: The label to search the button for.
+### clickPlaceholderButton
+
+Clicks a button in a placeholder based on the label text.
+
+_Parameters_
+
+- _buttonText_ `string`: The text that appears on the button to click.
+
### closeGlobalBlockInserter
Undocumented declaration.
diff --git a/packages/e2e-test-utils/src/click-placeholder-button.js b/packages/e2e-test-utils/src/click-placeholder-button.js
new file mode 100644
index 00000000000000..25b4d7a3a90c01
--- /dev/null
+++ b/packages/e2e-test-utils/src/click-placeholder-button.js
@@ -0,0 +1,32 @@
+/**
+ * Clicks a button in a placeholder based on the label text.
+ *
+ * @param {string} buttonText The text that appears on the button to click.
+ */
+export async function clickPlaceholderButton( buttonText ) {
+ const _button = await page.waitForFunction(
+ ( text ) => {
+ const placeholders = document.querySelectorAll(
+ '.wp-block-editor-placeholder'
+ );
+
+ for ( const placeholder of placeholders ) {
+ const buttons = placeholder.shadowRoot.querySelectorAll(
+ 'button,label,[aria-label]'
+ );
+
+ for ( const button of buttons ) {
+ if (
+ button.textContent === text ||
+ button.getAttribute( 'aria-label' ) === text
+ ) {
+ return button;
+ }
+ }
+ }
+ },
+ {},
+ buttonText
+ );
+ await _button.click();
+}
diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js
index f138d7a121faef..3fb3498671bf85 100644
--- a/packages/e2e-test-utils/src/index.js
+++ b/packages/e2e-test-utils/src/index.js
@@ -14,6 +14,7 @@ export { clickButton } from './click-button';
export { clickMenuItem } from './click-menu-item';
export { clickOnCloseModalButton } from './click-on-close-modal-button';
export { clickOnMoreMenuItem } from './click-on-more-menu-item';
+export { clickPlaceholderButton } from './click-placeholder-button';
export { createNewPost } from './create-new-post';
export { createUser } from './create-user';
export { createURL } from './create-url';
diff --git a/packages/e2e-tests/specs/editor/blocks/columns.test.js b/packages/e2e-tests/specs/editor/blocks/columns.test.js
index f593800cd0970a..25a6436dcc41a3 100644
--- a/packages/e2e-tests/specs/editor/blocks/columns.test.js
+++ b/packages/e2e-tests/specs/editor/blocks/columns.test.js
@@ -7,6 +7,7 @@ import {
insertBlock,
openGlobalBlockInserter,
closeGlobalBlockInserter,
+ clickPlaceholderButton,
} from '@wordpress/e2e-test-utils';
describe( 'Columns', () => {
@@ -17,7 +18,7 @@ describe( 'Columns', () => {
it( 'restricts all blocks inside the columns block', async () => {
await insertBlock( 'Columns' );
await closeGlobalBlockInserter();
- await page.click( '[aria-label="Two columns; equal split"]' );
+ await clickPlaceholderButton( 'Two columns; equal split' );
await page.click( '.edit-post-header-toolbar__list-view-toggle' );
const columnBlockMenuItem = (
await page.$x(
diff --git a/packages/e2e-tests/specs/editor/blocks/gallery.test.js b/packages/e2e-tests/specs/editor/blocks/gallery.test.js
index 50a10288626c41..91190b338b0e9d 100644
--- a/packages/e2e-tests/specs/editor/blocks/gallery.test.js
+++ b/packages/e2e-tests/specs/editor/blocks/gallery.test.js
@@ -16,9 +16,16 @@ import {
clickButton,
} from '@wordpress/e2e-test-utils';
-async function upload( selector ) {
- await page.waitForSelector( selector );
- const inputElement = await page.$( selector );
+async function placeholderUpload() {
+ const input = await page.waitForFunction( () =>
+ document
+ .querySelector( '.wp-block-gallery .wp-block-editor-placeholder' )
+ ?.shadowRoot.querySelector( 'input[type="file"]' )
+ );
+ return upload( input );
+}
+
+async function upload( handle ) {
const testImagePath = path.join(
__dirname,
'..',
@@ -30,7 +37,7 @@ async function upload( selector ) {
const filename = uuid();
const tmpFileName = path.join( os.tmpdir(), filename + '.png' );
fs.copyFileSync( testImagePath, tmpFileName );
- await inputElement.uploadFile( tmpFileName );
+ await handle.uploadFile( tmpFileName );
await page.waitForSelector(
`.wp-block-gallery img[src$="${ filename }.png"]`
);
@@ -44,7 +51,7 @@ describe( 'Gallery', () => {
it( 'can be created using uploaded images', async () => {
await insertBlock( 'Gallery' );
- const filename = await upload( '.wp-block-gallery input[type="file"]' );
+ const filename = await placeholderUpload();
const regex = new RegExp(
`\\s*