From 1b15d4ce7a0439efc26cb3c31ca03c2dedc4cc54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Tue, 14 Dec 2021 13:31:25 +0200 Subject: [PATCH] Block editor: placeholders: try admin shadow --- package-lock.json | 1 + packages/block-editor/README.md | 13 ++ .../use-focus-first-element.js | 26 ++- .../block-variation-picker/index.js | 7 +- .../embedded-admin-context/index.js | 157 ++++++++++++++++++ packages/block-editor/src/components/index.js | 1 + .../src/components/media-placeholder/index.js | 2 +- .../src/components/placeholder/index.js | 33 ++++ .../components/writing-flow/use-tab-nav.js | 29 +--- packages/block-library/src/block/edit.js | 2 +- packages/block-library/src/calendar/edit.js | 4 +- packages/block-library/src/categories/edit.js | 7 +- .../src/embed/embed-placeholder.js | 4 +- .../block-library/src/embed/embed-preview.js | 4 +- .../block-library/src/latest-posts/edit.js | 2 +- .../block-library/src/post-comment/edit.js | 8 +- packages/block-library/src/rss/edit.js | 2 +- packages/block-library/src/site-logo/edit.js | 2 +- .../src/table-of-contents/edit.js | 2 +- packages/block-library/src/table/edit.js | 2 +- .../template-part/edit/placeholder/index.js | 3 +- .../components/src/placeholder/test/index.js | 2 +- packages/dom/src/focusable.js | 16 +- packages/dom/src/tabbable.js | 11 +- packages/e2e-test-utils/README.md | 8 + .../src/click-placeholder-button.js | 32 ++++ packages/e2e-test-utils/src/index.js | 1 + .../specs/editor/blocks/columns.test.js | 3 +- .../specs/editor/blocks/gallery.test.js | 21 ++- .../specs/editor/blocks/image.test.js | 41 +++-- .../specs/editor/blocks/table.test.js | 48 ++---- .../editor/plugins/block-variations.test.js | 6 +- .../specs/editor/plugins/cpt-locking.test.js | 4 +- .../specs/editor/plugins/image-size.test.js | 9 +- .../plugins/innerblocks-locking-all-embed.js | 6 +- .../editor/various/block-deletion.test.js | 2 +- .../block-hierarchy-navigation.test.js | 5 +- .../specs/editor/various/embedding.test.js | 63 +++---- .../various/multi-block-selection.test.js | 10 +- .../various/toolbar-roving-tabindex.test.js | 3 +- .../specs/editor/various/writing-flow.test.js | 25 +-- .../site-editor/multi-entity-saving.test.js | 9 +- .../specs/site-editor/template-part.test.js | 16 +- .../specs/widgets/customizing-widgets.test.js | 9 +- packages/server-side-render/package.json | 1 + .../src/server-side-render.js | 3 +- .../src/blocks/legacy-widget/edit/index.js | 3 +- .../src/blocks/legacy-widget/edit/preview.js | 3 +- 48 files changed, 459 insertions(+), 212 deletions(-) create mode 100644 packages/block-editor/src/components/embedded-admin-context/index.js create mode 100644 packages/block-editor/src/components/placeholder/index.js create mode 100644 packages/e2e-test-utils/src/click-placeholder-button.js diff --git a/package-lock.json b/package-lock.json index d90064968229a..eb9f9fb408f63 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 94a227890243b..207dd8788d963 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 a3e522b080af8..7095bd7e2dbc8 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 24bdfa272c34b..262f5e22b3e49 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 0000000000000..3db90e1b45c72 --- /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 b1f991ce5c4d5..5deb4fb7483ee 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 f13206870f815..df321e0778fee 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 0000000000000..da0f1a24c0f10 --- /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 7f61046809d71..8ceb5e0375d47 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 ce2bb4c89b90a..a6f671a0248e9 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 5ee7fe14ed072..cb3137b6ee9a3 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 08276c026b302..78dcc9b2da3a3 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 e3ffb5ac02845..858033811ab5d 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 9e04efe78a0bf..15116f9607539 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 0f8d41b758957..e10504282cab7 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 e78ecd47bab16..a68ed2c0eda12 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 46a3e6a1a105d..ad6ddb23361a3 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 6719a4244ab8b..7f26a6c4caab5 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 44cd67303c745..796d775ff16cf 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 113dd178b3caf..5989b857237df 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 64a730535c0f5..91a186061fae0 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 758c6bed30394..69fba05049cb2 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 75e4f72f0c1f7..b3c6ce8ef7a41 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 64cef7f2a2485..5e5200a0f1758 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 210c15c101433..e83bda92d5e2b 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 0000000000000..25b4d7a3a90c0 --- /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 f138d7a121fae..3fb3498671bf8 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 f593800cd0970..25a6436dcc41a 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 50a10288626c4..91190b338b0e9 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*