diff --git a/.gitignore b/.gitignore index b5cd0124e5d4bf..48cd7580beed14 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ test/storybook-playwright/specs/__snapshots__ test/storybook-playwright/specs/*-snapshots/** test/gutenberg-test-themes/twentytwentyone test/gutenberg-test-themes/twentytwentythree +packages/react-native-editor/src/setup-local.js diff --git a/docs/contributors/code/react-native/getting-started-react-native.md b/docs/contributors/code/react-native/getting-started-react-native.md index e13ac748aaf293..53d5f7eee5a124 100644 --- a/docs/contributors/code/react-native/getting-started-react-native.md +++ b/docs/contributors/code/react-native/getting-started-react-native.md @@ -71,6 +71,40 @@ npm run native ios -- -- --simulator="iPhone Xs Max" To see a list of all of your available iOS devices, use `xcrun simctl list devices`. +### Customizing the Demo Editor + +By default, the Demo editor renders most of the supported core blocks. This is helpful to showcase the editor's capabilities, but can be distracting when focusing on a specific block or feature. One can customize the editor's intial state by leveraging the `native.block_editor_props` hook in a `packages/react-native-editor/src/setup-local.js` file. + +
Example setup-local.js + +```js +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; + +export default () => { + addFilter( + 'native.block_editor_props', + 'core/react-native-editor', + ( props ) => { + return { + ...props, + initialHtml, + }; + } + ); +}; + +const initialHtml = ` + +

Just a Heading

+ +`; +``` + +
+ ### Troubleshooting If the Android emulator doesn't start correctly, or compiling fails with `Could not initialize class org.codehaus.groovy.runtime.InvokerHelper` or similar, it may help to double check the set up of your development environment against the latest requirements in [React Native's documentation](https://reactnative.dev/docs/environment-setup). With Android Studio, for example, you will need to configure the `ANDROID_HOME` environment variable and ensure that your version of JDK matches the latest requirements. diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index a7570bf590a397..ffd45a7cb8c75d 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -274,7 +274,46 @@ Display footnotes added to the page. ([Source](https://github.com/WordPress/gute - **Name:** core/footnotes - **Category:** text -- **Supports:** color (background, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~, ~~multiple~~, ~~reusable~~ +- **Supports:** color (background, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~, ~~inserter~~, ~~multiple~~, ~~reusable~~ +- **Attributes:** + +## Form + +A form. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/form)) + +- **Name:** core/form +- **Category:** common +- **Supports:** anchor, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ +- **Attributes:** action, email, method, submissionMethod + +## Input field + +The basic building block for forms. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/form-input)) + +- **Name:** core/form-input +- **Category:** common +- **Parent:** core/form +- **Supports:** anchor, spacing (margin), ~~reusable~~ +- **Attributes:** inlineLabel, label, name, placeholder, required, type, value, visibilityPermissions + +## Form Submission Notification + +Provide a notification message after the form has been submitted. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/form-submission-notification)) + +- **Name:** core/form-submission-notification +- **Category:** common +- **Parent:** core/form +- **Supports:** +- **Attributes:** type + +## Form submit button + +A submission button for forms. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/form-submit-button)) + +- **Name:** core/form-submit-button +- **Category:** common +- **Parent:** core/form +- **Supports:** - **Attributes:** ## Classic diff --git a/lib/blocks.php b/lib/blocks.php index 537fa9ce4b45e1..1794762b010dbd 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -22,6 +22,8 @@ function gutenberg_reregister_core_block_types() { 'column', 'columns', 'details', + 'form-input', + 'form-submit-button', 'group', 'html', 'list', @@ -66,6 +68,9 @@ function gutenberg_reregister_core_block_types() { 'comments.php' => 'core/comments', 'footnotes.php' => 'core/footnotes', 'file.php' => 'core/file', + 'form.php' => 'core/form', + 'form-input.php' => 'core/form-input', + 'form-submission-notification.php' => 'core/form-submission-notification', 'home-link.php' => 'core/home-link', 'image.php' => 'core/image', 'gallery.php' => 'core/gallery', diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index f5cc2ec969d334..2c7d6310005bfa 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -36,3 +36,15 @@ function gutenberg_enable_experiments() { } add_action( 'admin_init', 'gutenberg_enable_experiments' ); + +/** + * Sets a global JS variable used to trigger the availability of form & input blocks. + */ +function gutenberg_enable_form_input_blocks() { + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-form-blocks', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableFormBlocks = true', 'before' ); + } +} + +add_action( 'admin_init', 'gutenberg_enable_form_input_blocks' ); diff --git a/lib/experimental/kses-allowed-html.php b/lib/experimental/kses-allowed-html.php new file mode 100644 index 00000000000000..122faef7b4ca2c --- /dev/null +++ b/lib/experimental/kses-allowed-html.php @@ -0,0 +1,43 @@ + array(), + 'name' => array(), + 'value' => array(), + 'checked' => array(), + 'required' => array(), + 'aria-required' => array(), + 'class' => array(), + ); + + $allowedtags['label'] = array( + 'for' => array(), + 'class' => array(), + ); + + $allowedtags['textarea'] = array( + 'name' => array(), + 'required' => array(), + 'aria-required' => array(), + 'class' => array(), + ); + return $allowedtags; +} +add_filter( 'wp_kses_allowed_html', 'gutenberg_kses_allowed_html', 10, 2 ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 3bf53efd616220..0bcd28b2aa2c49 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -90,6 +90,17 @@ function gutenberg_initialize_experiments_settings() { 'id' => 'gutenberg-color-randomizer', ) ); + add_settings_field( + 'gutenberg-form-blocks', + __( 'Form and input blocks ', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Test new blocks to allow building forms (Warning: The new feature is not ready. You may experience UX issues that are being addressed)', 'gutenberg' ), + 'id' => 'gutenberg-form-blocks', + ) + ); add_settings_field( 'gutenberg-group-grid-variation', diff --git a/lib/load.php b/lib/load.php index d87c53081903ea..a3a61407764b5c 100644 --- a/lib/load.php +++ b/lib/load.php @@ -66,6 +66,8 @@ function gutenberg_is_experiment_enabled( $name ) { } require_once __DIR__ . '/experimental/class-gutenberg-rest-template-revision-count.php'; require_once __DIR__ . '/experimental/rest-api.php'; + + require_once __DIR__ . '/experimental/kses-allowed-html.php'; } require __DIR__ . '/experimental/editor-settings.php'; diff --git a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap index 516fa70a321e9b..242218705b3cff 100644 --- a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap +++ b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap @@ -219,10 +219,11 @@ exports[`ColorPaletteControl matches the snapshot 1`] = ` aria-label="Color: red" aria-selected="true" class="components-button components-circular-option-picker__option" + data-active-item="" + data-command="" id="components-circular-option-picker-0-0" role="option" style="background-color: rgb(255, 0, 0); color: rgb(255, 0, 0);" - tabindex="0" type="button" /> ` maintains an internal state tracking temporary user edits to the link `value` prior to submission. To avoid unwanted synchronization of this internal value, it is advised that the `value` prop is stablized (likely via memozation) before it is passed to the component. This will avoid unwanted loss of any changes users have may made whilst interacting with the control. + +```jsx +const memoizedValue = useMemo( + () => ( { + url: attributes.url, + type: attributes.type, + opensInNewTab: attributes.target === '_blank', + title: attributes.text, + } ), + [ + attributes.url, + attributes.type, + attributes.target, + attributes.text, + ] +); + + +``` + ### settings - Type: `Array` diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 8c1afdd8903bd0..32bd1afd3d5404 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -21,6 +21,7 @@ import { __unstableCreateElement, isEmpty, insert, + remove, create, split, toHTMLString, @@ -70,6 +71,7 @@ function RichTextWrapper( onSplit, __unstableOnSplitAtEnd: onSplitAtEnd, __unstableOnSplitMiddle: onSplitMiddle, + __unstableOnSplitAtDoubleLineEnd: onSplitAtDoubleLineEnd, identifier, preserveWhiteSpace, __unstablePastePlainText: pastePlainText, @@ -339,14 +341,28 @@ function RichTextWrapper( splitStart === splitEnd && splitEnd === text.length; - if ( shiftKey || ( ! canSplit && ! canSplitAtEnd ) ) { + if ( shiftKey ) { if ( ! disableLineBreaks ) { onChange( insert( value, '\n' ) ); } - } else if ( ! canSplit && canSplitAtEnd ) { - onSplitAtEnd(); } else if ( canSplit ) { splitValue( value ); + } else if ( canSplitAtEnd ) { + onSplitAtEnd(); + } else if ( + // For some blocks it's desirable to split at the end of the + // block when there are two line breaks at the end of the + // block, so triple Enter exits the block. + onSplitAtDoubleLineEnd && + splitStart === splitEnd && + splitEnd === text.length && + text.slice( -2 ) === '\n\n' + ) { + value.start = value.end - 2; + onChange( remove( value ) ); + onSplitAtDoubleLineEnd(); + } else if ( ! disableLineBreaks ) { + onChange( insert( value, '\n' ) ); } }, // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/block-library/src/code/edit.native.js b/packages/block-library/src/code/edit.native.js index 58188d0b2119f0..3353dbc3c25a01 100644 --- a/packages/block-library/src/code/edit.native.js +++ b/packages/block-library/src/code/edit.native.js @@ -9,6 +9,7 @@ import { View } from 'react-native'; import { PlainText } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { usePreferredColorSchemeStyle } from '@wordpress/compose'; +import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; /** * Internal dependencies @@ -22,7 +23,15 @@ import styles from './theme.scss'; // Note: styling is applied directly to the (nested) PlainText component. Web-side components // apply it to the container 'div' but we don't have a proper proposal for cascading styling yet. export function CodeEdit( props ) { - const { attributes, setAttributes, onFocus, onBlur, style } = props; + const { + attributes, + setAttributes, + onFocus, + onBlur, + style, + insertBlocksAfter, + mergeBlocks, + } = props; const codeStyle = { ...usePreferredColorSchemeStyle( styles.blockCode, @@ -40,16 +49,21 @@ export function CodeEdit( props ) { setAttributes( { content } ) } + onMerge={ mergeBlocks } placeholder={ __( 'Write code…' ) } aria-label={ __( 'Code' ) } isSelected={ props.isSelected } onFocus={ onFocus } onBlur={ onBlur } placeholderTextColor={ placeholderStyle.color } + __unstableOnSplitAtDoubleLineEnd={ () => + insertBlocksAfter( createBlock( getDefaultBlockName() ) ) + } /> ); diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 07c58599c50980..5f3d962ae7afae 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -16,6 +16,8 @@ @import "./details/editor.scss"; @import "./embed/editor.scss"; @import "./file/editor.scss"; +@import "./form-input/editor.scss"; +@import "./form-submission-notification/editor.scss"; @import "./freeform/editor.scss"; @import "./gallery/editor.scss"; @import "./group/editor.scss"; diff --git a/packages/block-library/src/footnotes/block.json b/packages/block-library/src/footnotes/block.json index 4a2db992863db2..3192df77969781 100644 --- a/packages/block-library/src/footnotes/block.json +++ b/packages/block-library/src/footnotes/block.json @@ -33,6 +33,7 @@ "html": false, "multiple": false, "reusable": false, + "inserter": false, "spacing": { "margin": true, "padding": true, diff --git a/packages/block-library/src/form-input/block.json b/packages/block-library/src/form-input/block.json new file mode 100644 index 00000000000000..dbe182f03b4992 --- /dev/null +++ b/packages/block-library/src/form-input/block.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "core/form-input", + "title": "Input field", + "category": "common", + "parent": [ "core/form" ], + "description": "The basic building block for forms.", + "keywords": [ "input", "form" ], + "textdomain": "default", + "icon": "forms", + "attributes": { + "type": { + "type": "string", + "default": "text" + }, + "name": { + "type": "string" + }, + "label": { + "type": "string", + "default": "Label", + "selector": ".wp-block-form-input__label-content", + "source": "html", + "__experimentalRole": "content" + }, + "inlineLabel": { + "type": "boolean", + "default": false + }, + "required": { + "type": "boolean", + "default": false, + "selector": ".wp-block-form-input__input", + "source": "attribute", + "attribute": "required" + }, + "placeholder": { + "type": "string", + "selector": ".wp-block-form-input__input", + "source": "attribute", + "attribute": "placeholder", + "__experimentalRole": "content" + }, + "value": { + "type": "string", + "default": "", + "selector": "input", + "source": "attribute", + "attribute": "value" + }, + "visibilityPermissions": { + "type": "string", + "default": "all" + } + }, + "supports": { + "anchor": true, + "reusable": false, + "spacing": { + "margin": [ "top", "bottom" ] + }, + "__experimentalBorder": { + "radius": true, + "__experimentalSkipSerialization": true, + "__experimentalDefaultControls": { + "radius": true + } + } + }, + "style": [ "wp-block-form-input" ] +} diff --git a/packages/block-library/src/form-input/edit.js b/packages/block-library/src/form-input/edit.js new file mode 100644 index 00000000000000..0742c22c22f429 --- /dev/null +++ b/packages/block-library/src/form-input/edit.js @@ -0,0 +1,151 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + InspectorControls, + RichText, + useBlockProps, + __experimentalUseBorderProps as useBorderProps, + __experimentalUseColorProps as useColorProps, +} from '@wordpress/block-editor'; +import { PanelBody, TextControl, CheckboxControl } from '@wordpress/components'; + +import { useRef } from '@wordpress/element'; + +function InputFieldBlock( { attributes, setAttributes, className } ) { + const { type, name, label, inlineLabel, required, placeholder, value } = + attributes; + const blockProps = useBlockProps(); + const ref = useRef(); + const TagName = type === 'textarea' ? 'textarea' : 'input'; + + const borderProps = useBorderProps( attributes ); + const colorProps = useColorProps( attributes ); + if ( ref.current ) { + ref.current.focus(); + } + + const controls = ( + <> + { 'hidden' !== type && ( + + + { 'checkbox' !== type && ( + { + setAttributes( { + inlineLabel: newVal, + } ); + } } + /> + ) } + { + setAttributes( { + required: newVal, + } ); + } } + /> + + + ) } + + { + setAttributes( { + name: newVal, + } ); + } } + help={ __( + 'Affects the "name" atribute of the input element, and is used as a name for the form submission results.' + ) } + /> + + + ); + + if ( 'hidden' === type ) { + return ( + <> + { controls } +
+ { controls } + + + setAttributes( { label: newLabel } ) + } + aria-label={ label ? __( 'Label' ) : __( 'Empty label' ) } + data-empty={ label ? false : true } + placeholder={ __( 'Type the label for this input' ) } + /> + + setAttributes( { placeholder: event.target.value } ) + } + aria-required={ required } + style={ { + ...borderProps.style, + ...colorProps.style, + } } + /> + +
+ ); +} + +export default InputFieldBlock; diff --git a/packages/block-library/src/form-input/editor.scss b/packages/block-library/src/form-input/editor.scss new file mode 100644 index 00000000000000..2ac67e6615ed4a --- /dev/null +++ b/packages/block-library/src/form-input/editor.scss @@ -0,0 +1,24 @@ +.wp-block-form-input { + .is-input-hidden { + font-size: 0.85em; + opacity: 0.3; + border: 1px dashed; + padding: 0.5em; + box-sizing: border-box; + background: repeating-linear-gradient(45deg, transparent, transparent 5px, currentColor 5px, currentColor 6px); + + input[type="text"] { + background: transparent; + } + } + &.is-selected { + .is-input-hidden { + opacity: 1; + background: none; + + input[type="text"] { + background: unset; + } + } + } +} diff --git a/packages/block-library/src/form-input/index.js b/packages/block-library/src/form-input/index.js new file mode 100644 index 00000000000000..b700e0ade6ca7f --- /dev/null +++ b/packages/block-library/src/form-input/index.js @@ -0,0 +1,20 @@ +/** + * Internal dependencies + */ +import initBlock from '../utils/init-block'; +import edit from './edit'; +import metadata from './block.json'; +import save from './save'; +import variations from './variations'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + edit, + save, + variations, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/form-input/index.php b/packages/block-library/src/form-input/index.php new file mode 100644 index 00000000000000..f905c2bc6e19ff --- /dev/null +++ b/packages/block-library/src/form-input/index.php @@ -0,0 +1,45 @@ + 'render_block_core_form_input', + ) + ); +} +add_action( 'init', 'register_block_core_form_input' ); diff --git a/packages/block-library/src/form-input/init.js b/packages/block-library/src/form-input/init.js new file mode 100644 index 00000000000000..79f0492c2cb2f8 --- /dev/null +++ b/packages/block-library/src/form-input/init.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import { init } from './'; + +export default init(); diff --git a/packages/block-library/src/form-input/save.js b/packages/block-library/src/form-input/save.js new file mode 100644 index 00000000000000..0cca31ca423ee6 --- /dev/null +++ b/packages/block-library/src/form-input/save.js @@ -0,0 +1,83 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; +import removeAccents from 'remove-accents'; + +/** + * WordPress dependencies + */ +import { + RichText, + __experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, + __experimentalGetColorClassesAndStyles as getColorClassesAndStyles, +} from '@wordpress/block-editor'; + +/** + * Get the name attribute from a content string. + * + * @param {string} content The block content. + * + * @return {string} Returns the slug. + */ +const getNameFromLabel = ( content ) => { + const dummyElement = document.createElement( 'div' ); + dummyElement.innerHTML = content; + // Get the slug. + return ( + removeAccents( dummyElement.innerText ) + // Convert anything that's not a letter or number to a hyphen. + .replace( /[^\p{L}\p{N}]+/gu, '-' ) + // Convert to lowercase + .toLowerCase() + // Remove any remaining leading or trailing hyphens. + .replace( /(^-+)|(-+$)/g, '' ) + ); +}; + +export default function save( { attributes } ) { + const { type, name, label, inlineLabel, required, placeholder, value } = + attributes; + + const borderProps = getBorderClassesAndStyles( attributes ); + const colorProps = getColorClassesAndStyles( attributes ); + + const inputStyle = { + ...borderProps.style, + ...colorProps.style, + }; + + const inputClasses = classNames( + 'wp-block-form-input__input', + colorProps.className, + borderProps.className + ); + const TagName = type === 'textarea' ? 'textarea' : 'input'; + + if ( 'hidden' === type ) { + return ; + } + + /* eslint-disable jsx-a11y/label-has-associated-control */ + return ( + + ); + /* eslint-enable jsx-a11y/label-has-associated-control */ +} diff --git a/packages/block-library/src/form-input/style.scss b/packages/block-library/src/form-input/style.scss new file mode 100644 index 00000000000000..d45fc8d7f1f729 --- /dev/null +++ b/packages/block-library/src/form-input/style.scss @@ -0,0 +1,61 @@ +.wp-block-form-input__label { + width: 100%; + display: flex; + flex-direction: column; + gap: 0.25em; + margin-bottom: 0.5em; + + &.is-label-inline { + flex-direction: row; + gap: 0.5em; + align-items: center; + + .wp-block-form-input__label-content { + margin-bottom: 0.5em; + } + } + + /* + Small tweak to left-align the checkbox. + Even though `:has` is not currently supported in Firefox, this is a small tweak + and does not affect the functionality of the block or the user's experience. + There will be a minor inconsistency between browsers. However, it's more important to provide + a better experience for 80+% of users, until Firefox catches up and supports `:has`. + */ + &:has(input[type="checkbox"]) { + width: fit-content; + flex-direction: row-reverse; + } +} + +.wp-block-form-input__label-content { + width: fit-content; +} + +.wp-block-form-input__input { + padding: 0 0.5em; + font-size: 1em; + margin-bottom: 0.5em; + + &[type="text"], + &[type="password"], + &[type="date"], + &[type="datetime"], + &[type="datetime-local"], + &[type="email"], + &[type="month"], + &[type="number"], + &[type="search"], + &[type="tel"], + &[type="time"], + &[type="url"], + &[type="week"] { + min-height: 2em; + line-height: 2; + border: 1px solid; + } +} + +textarea.wp-block-form-input__input { + min-height: 10em; +} diff --git a/packages/block-library/src/form-input/variations.js b/packages/block-library/src/form-input/variations.js new file mode 100644 index 00000000000000..cc205feb895016 --- /dev/null +++ b/packages/block-library/src/form-input/variations.js @@ -0,0 +1,82 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +const variations = [ + { + name: 'text', + title: __( 'Text input' ), + icon: 'edit-page', + description: __( 'A generic text input.' ), + attributes: { type: 'text' }, + isDefault: true, + scope: [ 'inserter', 'transform' ], + isActive: ( blockAttributes ) => + ! blockAttributes?.type || blockAttributes?.type === 'text', + }, + { + name: 'textarea', + title: __( 'Textarea input' ), + icon: 'testimonial', + description: __( + 'A textarea input to allow entering multiple lines of text.' + ), + attributes: { type: 'textarea' }, + isDefault: true, + scope: [ 'inserter', 'transform' ], + isActive: ( blockAttributes ) => blockAttributes?.type === 'textarea', + }, + { + name: 'checkbox', + title: __( 'Checkbox input' ), + description: __( 'A simple checkbox input.' ), + icon: 'forms', + attributes: { type: 'checkbox', inlineLabel: true }, + isDefault: true, + scope: [ 'inserter', 'transform' ], + isActive: ( blockAttributes ) => blockAttributes?.type === 'checkbox', + }, + { + name: 'email', + title: __( 'Email input' ), + icon: 'email', + description: __( 'Used for email addresses.' ), + attributes: { type: 'email' }, + isDefault: true, + scope: [ 'inserter', 'transform' ], + isActive: ( blockAttributes ) => blockAttributes?.type === 'email', + }, + { + name: 'url', + title: __( 'URL input' ), + icon: 'admin-site', + description: __( 'Used for URLs.' ), + attributes: { type: 'url' }, + isDefault: true, + scope: [ 'inserter', 'transform' ], + isActive: ( blockAttributes ) => blockAttributes?.type === 'url', + }, + { + name: 'tel', + title: __( 'Telephone input' ), + icon: 'phone', + description: __( 'Used for phone numbers.' ), + attributes: { type: 'tel' }, + isDefault: true, + scope: [ 'inserter', 'transform' ], + isActive: ( blockAttributes ) => blockAttributes?.type === 'tel', + }, + { + name: 'number', + title: __( 'Number input' ), + icon: 'edit-page', + description: __( 'A numeric input.' ), + attributes: { type: 'number' }, + isDefault: true, + scope: [ 'inserter', 'transform' ], + isActive: ( blockAttributes ) => blockAttributes?.type === 'number', + }, +]; + +export default variations; diff --git a/packages/block-library/src/form-submission-notification/block.json b/packages/block-library/src/form-submission-notification/block.json new file mode 100644 index 00000000000000..62284d35ab4ddd --- /dev/null +++ b/packages/block-library/src/form-submission-notification/block.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "core/form-submission-notification", + "title": "Form Submission Notification", + "category": "common", + "parent": [ "core/form" ], + "description": "Provide a notification message after the form has been submitted.", + "keywords": [ "form", "feedback", "notification", "message" ], + "textdomain": "default", + "icon": "feedback", + "attributes": { + "type": { + "type": "string", + "default": "success" + } + } +} diff --git a/packages/block-library/src/form-submission-notification/edit.js b/packages/block-library/src/form-submission-notification/edit.js new file mode 100644 index 00000000000000..4425a4d9147dfb --- /dev/null +++ b/packages/block-library/src/form-submission-notification/edit.js @@ -0,0 +1,63 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + InnerBlocks, + useBlockProps, + useInnerBlocksProps, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; + +/** + * External dependencies + */ +import classnames from 'classnames'; + +const TEMPLATE = [ + [ + 'core/paragraph', + { + content: __( + "Enter the message you wish displayed for form submission error/success, and select the type of the message (success/error) from the block's options." + ), + }, + ], +]; + +const Edit = ( { attributes, clientId } ) => { + const { type } = attributes; + const blockProps = useBlockProps( { + className: classnames( 'wp-block-form-submission-notification', { + [ `form-notification-type-${ type }` ]: type, + } ), + } ); + + const { hasInnerBlocks } = useSelect( + ( select ) => { + const { getBlock } = select( blockEditorStore ); + const block = getBlock( clientId ); + return { + hasInnerBlocks: !! ( block && block.innerBlocks.length ), + }; + }, + [ clientId ] + ); + + const innerBlocksProps = useInnerBlocksProps( blockProps, { + template: TEMPLATE, + renderAppender: hasInnerBlocks + ? undefined + : InnerBlocks.ButtonBlockAppender, + } ); + + return ( +
+ ); +}; +export default Edit; diff --git a/packages/block-library/src/form-submission-notification/editor.scss b/packages/block-library/src/form-submission-notification/editor.scss new file mode 100644 index 00000000000000..a8d3f4e3d92630 --- /dev/null +++ b/packages/block-library/src/form-submission-notification/editor.scss @@ -0,0 +1,45 @@ +.wp-block-form-submission-notification { + > * { + opacity: 0.25; + border: 1px dashed; + box-sizing: border-box; + background: repeating-linear-gradient(45deg, transparent, transparent 5px, currentColor 5px, currentColor 6px); + } + + &.is-selected, + &:has(.is-selected) { + > * { + opacity: 1; + background: none; + } + + &::after { + display: none !important; + } + } + + &::after { + display: flex; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + justify-content: center; + align-items: center; + font-size: 1.1em; + // font-weight: bold; + } + + &.form-notification-type-success { + &::after { + content: attr(data-message-success); + } + } + + &.form-notification-type-error { + &::after { + content: attr(data-message-error); + } + } +} diff --git a/packages/block-library/src/form-submission-notification/index.js b/packages/block-library/src/form-submission-notification/index.js new file mode 100644 index 00000000000000..67c359374eec1c --- /dev/null +++ b/packages/block-library/src/form-submission-notification/index.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { group as icon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import initBlock from '../utils/init-block'; +import edit from './edit'; +import metadata from './block.json'; +import save from './save'; +import variations from './variations'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + icon, + edit, + save, + variations, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/form-submission-notification/index.php b/packages/block-library/src/form-submission-notification/index.php new file mode 100644 index 00000000000000..0a57866f37edfc --- /dev/null +++ b/packages/block-library/src/form-submission-notification/index.php @@ -0,0 +1,48 @@ + 'render_block_core_form_submission_notification', + ) + ); +} +add_action( 'init', 'register_block_core_form_submission_notification' ); diff --git a/packages/block-library/src/form-submission-notification/init.js b/packages/block-library/src/form-submission-notification/init.js new file mode 100644 index 00000000000000..a7f22ef02d640f --- /dev/null +++ b/packages/block-library/src/form-submission-notification/init.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import { init } from '.'; + +export default init(); diff --git a/packages/block-library/src/form-submission-notification/save.js b/packages/block-library/src/form-submission-notification/save.js new file mode 100644 index 00000000000000..7b3c6c895c192a --- /dev/null +++ b/packages/block-library/src/form-submission-notification/save.js @@ -0,0 +1,28 @@ +/** + * WordPress dependencies + */ +import { useInnerBlocksProps, useBlockProps } from '@wordpress/block-editor'; + +/** + * External dependencies + */ +import classnames from 'classnames'; + +export default function save( { attributes } ) { + const { type } = attributes; + + return ( +
+ ); +} diff --git a/packages/block-library/src/form-submission-notification/variations.js b/packages/block-library/src/form-submission-notification/variations.js new file mode 100644 index 00000000000000..b154a26e5e6a43 --- /dev/null +++ b/packages/block-library/src/form-submission-notification/variations.js @@ -0,0 +1,59 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +const variations = [ + { + name: 'form-submission-success', + title: __( 'Form submission success' ), + description: __( 'Success message for form submissions' ), + attributes: { + type: 'success', + }, + isDefault: true, + innerBlocks: [ + [ + 'core/paragraph', + { + content: __( 'Your form has been submitted successfully.' ), + backgroundColor: '#00D084', + textColor: '#000000', + style: { + elements: { link: { color: { text: '#000000' } } }, + }, + }, + ], + ], + scope: [ 'inserter', 'transform' ], + isActive: ( blockAttributes ) => + ! blockAttributes?.type || blockAttributes?.type === 'success', + }, + { + name: 'form-submission-error', + title: __( 'Form submission error' ), + description: __( 'Error/failure message for form submissions' ), + attributes: { + type: 'error', + }, + isDefault: false, + innerBlocks: [ + [ + 'core/paragraph', + { + content: __( 'There was an error submitting your form.' ), + backgroundColor: '#CF2E2E', + textColor: '#FFFFFF', + style: { + elements: { link: { color: { text: '#FFFFFF' } } }, + }, + }, + ], + ], + scope: [ 'inserter', 'transform' ], + isActive: ( blockAttributes ) => + ! blockAttributes?.type || blockAttributes?.type === 'error', + }, +]; + +export default variations; diff --git a/packages/block-library/src/form-submit-button/block.json b/packages/block-library/src/form-submit-button/block.json new file mode 100644 index 00000000000000..faa938e9bbc244 --- /dev/null +++ b/packages/block-library/src/form-submit-button/block.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "core/form-submit-button", + "title": "Form submit button", + "category": "common", + "icon": "button", + "parent": [ "core/form" ], + "description": "A submission button for forms.", + "keywords": [ "submit", "button", "form" ], + "textdomain": "default", + "style": [ "wp-block-form-submit-button" ] +} diff --git a/packages/block-library/src/form-submit-button/edit.js b/packages/block-library/src/form-submit-button/edit.js new file mode 100644 index 00000000000000..f8d7a65c6877a6 --- /dev/null +++ b/packages/block-library/src/form-submit-button/edit.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; + +const TEMPLATE = [ + [ + 'core/buttons', + {}, + [ + [ + 'core/button', + { + text: __( 'Submit' ), + tagName: 'button', + }, + ], + ], + ], +]; +const Edit = () => { + const blockProps = useBlockProps(); + const innerBlocksProps = useInnerBlocksProps( blockProps, { + allowedBlocks: TEMPLATE, + template: TEMPLATE, + templateLock: 'all', + } ); + return ( +
+ ); +}; +export default Edit; diff --git a/packages/block-library/src/form-submit-button/index.js b/packages/block-library/src/form-submit-button/index.js new file mode 100644 index 00000000000000..4c60b5f5c20639 --- /dev/null +++ b/packages/block-library/src/form-submit-button/index.js @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import initBlock from '../utils/init-block'; +import edit from './edit'; +import metadata from './block.json'; +import save from './save'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + edit, + save, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/form-submit-button/init.js b/packages/block-library/src/form-submit-button/init.js new file mode 100644 index 00000000000000..79f0492c2cb2f8 --- /dev/null +++ b/packages/block-library/src/form-submit-button/init.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import { init } from './'; + +export default init(); diff --git a/packages/block-library/src/form-submit-button/save.js b/packages/block-library/src/form-submit-button/save.js new file mode 100644 index 00000000000000..ba361ebe9db209 --- /dev/null +++ b/packages/block-library/src/form-submit-button/save.js @@ -0,0 +1,14 @@ +/** + * WordPress dependencies + */ +import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; + +const Save = () => { + const blockProps = useBlockProps.save(); + return ( +
+ +
+ ); +}; +export default Save; diff --git a/packages/block-library/src/form-submit-button/style.scss b/packages/block-library/src/form-submit-button/style.scss new file mode 100644 index 00000000000000..400016b1618d47 --- /dev/null +++ b/packages/block-library/src/form-submit-button/style.scss @@ -0,0 +1,3 @@ +.wp-block-form-submit-wrapper { + margin-bottom: 0.5em; +} diff --git a/packages/block-library/src/form/block.json b/packages/block-library/src/form/block.json new file mode 100644 index 00000000000000..951d1dce4224eb --- /dev/null +++ b/packages/block-library/src/form/block.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "core/form", + "title": "Form", + "category": "common", + "description": "A form.", + "keywords": [ "container", "wrapper", "row", "section" ], + "textdomain": "default", + "icon": "feedback", + "attributes": { + "submissionMethod": { + "type": "string", + "default": "email" + }, + "method": { + "type": "string", + "default": "post" + }, + "action": { + "type": "string" + }, + "email": { + "type": "string" + } + }, + "supports": { + "anchor": true, + "className": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalTextDecoration": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalTextTransform": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__experimentalSelector": "form" + }, + "viewScript": "file:./view.min.js" +} diff --git a/packages/block-library/src/form/edit.js b/packages/block-library/src/form/edit.js new file mode 100644 index 00000000000000..d8ae9ea5e75539 --- /dev/null +++ b/packages/block-library/src/form/edit.js @@ -0,0 +1,179 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + InnerBlocks, + useBlockProps, + useInnerBlocksProps, + InspectorControls, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { TextControl, SelectControl, PanelBody } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { + formSubmissionNotificationSuccess, + formSubmissionNotificationError, +} from './utils.js'; + +const ALLOWED_BLOCKS = [ + 'core/paragraph', + 'core/heading', + 'core/form-input', + 'core/form-submit-button', + 'core/form-submission-notification', +]; + +const TEMPLATE = [ + formSubmissionNotificationSuccess, + formSubmissionNotificationError, + [ + 'core/form-input', + { + type: 'text', + label: __( 'Name' ), + required: true, + }, + ], + [ + 'core/form-input', + { + type: 'email', + label: __( 'Email' ), + required: true, + }, + ], + [ + 'core/form-input', + { + type: 'textarea', + label: __( 'Comment' ), + required: true, + }, + ], + [ 'core/form-submit-button', {} ], +]; + +const Edit = ( { attributes, setAttributes, clientId } ) => { + const { action, method, email, submissionMethod } = attributes; + const blockProps = useBlockProps(); + + const { hasInnerBlocks } = useSelect( + ( select ) => { + const { getBlock } = select( blockEditorStore ); + const block = getBlock( clientId ); + return { + hasInnerBlocks: !! ( block && block.innerBlocks.length ), + }; + }, + [ clientId ] + ); + + const innerBlocksProps = useInnerBlocksProps( blockProps, { + allowedBlocks: ALLOWED_BLOCKS, + template: TEMPLATE, + renderAppender: hasInnerBlocks + ? undefined + : InnerBlocks.ButtonBlockAppender, + } ); + + return ( + <> + + + + setAttributes( { submissionMethod: value } ) + } + help={ + submissionMethod === 'custom' + ? __( + 'Select the method to use for form submissions. Additional options for the "custom" mode can be found in the "Andvanced" section.' + ) + : __( + 'Select the method to use for form submissions.' + ) + } + /> + { submissionMethod === 'email' && ( + { + setAttributes( { email: value } ); + setAttributes( { + action: `mailto:${ value }`, + } ); + setAttributes( { method: 'post' } ); + } } + help={ __( + 'The email address where form submissions will be sent. Separate multiple email addresses with a comma.' + ) } + /> + ) } + + + { submissionMethod !== 'email' && ( + + + setAttributes( { method: value } ) + } + help={ __( + 'Select the method to use for form submissions.' + ) } + /> + { + setAttributes( { + action: newVal, + } ); + } } + help={ __( + 'The URL where the form should be submitted.' + ) } + /> + + ) } +
+ + ); +}; +export default Edit; diff --git a/packages/block-library/src/form/index.js b/packages/block-library/src/form/index.js new file mode 100644 index 00000000000000..b700e0ade6ca7f --- /dev/null +++ b/packages/block-library/src/form/index.js @@ -0,0 +1,20 @@ +/** + * Internal dependencies + */ +import initBlock from '../utils/init-block'; +import edit from './edit'; +import metadata from './block.json'; +import save from './save'; +import variations from './variations'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + edit, + save, + variations, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/form/index.php b/packages/block-library/src/form/index.php new file mode 100644 index 00000000000000..0dbbaf68387401 --- /dev/null +++ b/packages/block-library/src/form/index.php @@ -0,0 +1,218 @@ +next_tag( 'form' ); + + // Get the action for this form. + $action = ''; + if ( isset( $attributes['action'] ) ) { + $action = str_replace( + array( '{SITE_URL}', '{ADMIN_URL}' ), + array( site_url(), admin_url() ), + $attributes['action'] + ); + } + $processed_content->set_attribute( 'action', esc_attr( $action ) ); + + // Add the method attribute. If it is not set, default to `post`. + $method = empty( $attributes['method'] ) ? 'post' : $attributes['method']; + $processed_content->set_attribute( 'method', $method ); + + $extra_fields = apply_filters( 'render_block_core_form_extra_fields', '', $attributes ); + + return str_replace( + '
', + $extra_fields . '', + $processed_content->get_updated_html() + ); +} + +/** + * Additional data to add to the view.js script for this block. + */ +function block_core_form_view_script() { + if ( ! gutenberg_is_experiment_enabled( 'gutenberg-form-blocks' ) ) { + return; + } + + wp_localize_script( + 'wp-block-form-view', + 'wpBlockFormSettings', + array( + 'nonce' => wp_create_nonce( 'wp-block-form' ), + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'action' => 'wp_block_form_email_submit', + ) + ); +} +add_action( 'wp_enqueue_scripts', 'block_core_form_view_script' ); + +/** + * Adds extra fields to the form. + * + * If the form is a comment form, adds the post ID as a hidden field, + * to allow the comment to be associated with the post. + * + * @param string $extra_fields The extra fields. + * @param array $attributes The block attributes. + * + * @return string The extra fields. + */ +function block_core_form_extra_fields_comment_form( $extra_fields, $attributes ) { + if ( ! empty( $attributes['action'] ) && str_ends_with( $attributes['action'], '/wp-comments-post.php' ) ) { + $extra_fields .= ''; + } + return $extra_fields; +} +add_filter( 'render_block_core_form_extra_fields', 'block_core_form_extra_fields_comment_form', 10, 2 ); + +/** + * Sends an email if the form is a contact form. + * + * @return void + */ +function block_core_form_send_email() { + check_ajax_referer( 'wp-block-form' ); + + // Get the POST data. + $params = wp_unslash( $_POST ); + // Start building the email content. + $content = sprintf( + /* translators: %s: The request URI. */ + __( 'Form submission from %1$s', 'gutenberg' ) . '
', + '' . get_bloginfo( 'name' ) . '' + ); + + $skip_fields = array( 'formAction', '_ajax_nonce', 'action' ); + foreach ( $params as $key => $value ) { + if ( in_array( $key, $skip_fields, true ) ) { + continue; + } + $content .= sanitize_key( $key ) . ': ' . wp_kses_post( $value ) . '
'; + } + + // Filter the email content. + $content = apply_filters( 'render_block_core_form_email_content', $content, $params ); + + // Send the email. + $result = wp_mail( + str_replace( 'mailto:', '', $params['wp-email-address'] ), + __( 'Form submission', 'gutenberg' ), + $content + ); + + if ( ! $result ) { + wp_send_json_error( $result ); + } + wp_send_json_success( $result ); +} +add_action( 'wp_ajax_wp_block_form_email_submit', 'block_core_form_send_email' ); +add_action( 'wp_ajax_nopriv_wp_block_form_email_submit', 'block_core_form_send_email' ); + +/** + * Send the data export/remove request if the form is a privacy-request form. + * + * @return void + */ +function block_core_form_privacy_form() { + // Get the POST data. + $params = wp_unslash( $_POST ); + + // Bail early if not a form submission, or if the nonce is not valid. + if ( empty( $params['wp-action'] ) + || 'wp_privacy_send_request' !== $params['wp-action'] + || empty( $params['wp-privacy-request'] ) + || '1' !== $params['wp-privacy-request'] + || empty( $params['email'] ) + ) { + return; + } + + // Get the request types. + $request_types = _wp_privacy_action_request_types(); + $requests_found = array(); + foreach ( $request_types as $request_type ) { + if ( ! empty( $params[ $request_type ] ) ) { + $requests_found[] = $request_type; + } + } + + // Bail early if no requests were found. + if ( empty( $requests_found ) ) { + return; + } + + // Process the requests. + $actions_errored = array(); + $actions_performed = array(); + foreach ( $requests_found as $action_name ) { + // Get the request ID. + $request_id = wp_create_user_request( $params['email'], $action_name ); + + // Bail early if the request ID is invalid. + if ( is_wp_error( $request_id ) ) { + $actions_errored[] = $action_name; + continue; + } + + // Send the request email. + wp_send_user_request( $request_id ); + $actions_performed[] = $action_name; + } + + /** + * Determine whether the core/form-submission-notification block should be shown. + * + * @param bool $show Whether to show the core/form-submission-notification block. + * @param array $attributes The block attributes. + * + * @return bool Whether to show the core/form-submission-notification block. + */ + $show_notification = static function ( $show, $attributes ) use ( $actions_performed, $actions_errored ) { + switch ( $attributes['type'] ) { + case 'success': + return ! empty( $actions_performed ) && empty( $actions_errored ); + + case 'error': + return ! empty( $actions_errored ); + + default: + return $show; + } + }; + + // Add filter to show the core/form-submission-notification block. + add_filter( 'show_form_submission_notification_block', $show_notification, 10, 2 ); +} +add_action( 'wp', 'block_core_form_privacy_form' ); + +/** + * Registers the `core/form` block on server. + */ +function register_block_core_form() { + if ( ! gutenberg_is_experiment_enabled( 'gutenberg-form-blocks' ) ) { + return; + } + register_block_type_from_metadata( + __DIR__ . '/form', + array( + 'render_callback' => 'render_block_core_form', + ) + ); +} +add_action( 'init', 'register_block_core_form' ); diff --git a/packages/block-library/src/form/init.js b/packages/block-library/src/form/init.js new file mode 100644 index 00000000000000..79f0492c2cb2f8 --- /dev/null +++ b/packages/block-library/src/form/init.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import { init } from './'; + +export default init(); diff --git a/packages/block-library/src/form/save.js b/packages/block-library/src/form/save.js new file mode 100644 index 00000000000000..a824fc076d2ac3 --- /dev/null +++ b/packages/block-library/src/form/save.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; + +const Save = ( { attributes } ) => { + const blockProps = useBlockProps.save(); + const { submissionMethod } = attributes; + + return ( +
+ + + ); +}; +export default Save; diff --git a/packages/block-library/src/form/utils.js b/packages/block-library/src/form/utils.js new file mode 100644 index 00000000000000..e541f34bbc887f --- /dev/null +++ b/packages/block-library/src/form/utils.js @@ -0,0 +1,39 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +export const formSubmissionNotificationSuccess = [ + 'core/form-submission-notification', + { + type: 'success', + }, + [ + [ + 'core/paragraph', + { + content: + '' + + __( 'Your form has been submitted successfully' ) + + '', + }, + ], + ], +]; +export const formSubmissionNotificationError = [ + 'core/form-submission-notification', + { + type: 'error', + }, + [ + [ + 'core/paragraph', + { + content: + '' + + __( 'There was an error submitting your form.' ) + + '', + }, + ], + ], +]; diff --git a/packages/block-library/src/form/variations.js b/packages/block-library/src/form/variations.js new file mode 100644 index 00000000000000..da3fcbbf039421 --- /dev/null +++ b/packages/block-library/src/form/variations.js @@ -0,0 +1,139 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { + formSubmissionNotificationSuccess, + formSubmissionNotificationError, +} from './utils.js'; + +const variations = [ + { + name: 'comment-form', + title: __( 'Experimental Comment form' ), + description: __( 'A comment form for posts and pages.' ), + attributes: { + submissionMethod: 'custom', + action: '{SITE_URL}/wp-comments-post.php', + method: 'post', + anchor: 'comment-form', + }, + isDefault: false, + innerBlocks: [ + [ + 'core/form-input', + { + type: 'text', + name: 'author', + label: __( 'Name' ), + required: true, + visibilityPermissions: 'logged-out', + }, + ], + [ + 'core/form-input', + { + type: 'email', + name: 'email', + label: __( 'Email' ), + required: true, + visibilityPermissions: 'logged-out', + }, + ], + [ + 'core/form-input', + { + type: 'textarea', + name: 'comment', + label: __( 'Comment' ), + required: true, + visibilityPermissions: 'all', + }, + ], + [ 'core/form-submit-button', {} ], + ], + scope: [ 'inserter', 'transform' ], + isActive: ( blockAttributes ) => + ! blockAttributes?.type || blockAttributes?.type === 'text', + }, + { + name: 'wp-privacy-form', + title: __( 'Experimental privacy request form' ), + keywords: [ 'GDPR' ], + description: __( 'A form torequest data exports and/or deletion.' ), + attributes: { + submissionMethod: 'custom', + action: '', + method: 'post', + anchor: 'gdpr-form', + }, + isDefault: false, + innerBlocks: [ + formSubmissionNotificationSuccess, + formSubmissionNotificationError, + [ + 'core/paragraph', + { + content: __( + 'To request an export or deletion of your personal data on this site, please fill-in the form below. You can define the type of request you wish to perform, and your email address. Once the form is submitted, you will receive a confirmation email with instructions on the next steps.' + ), + }, + ], + [ + 'core/form-input', + { + type: 'email', + name: 'email', + label: __( 'Enter your email address.' ), + required: true, + visibilityPermissions: 'all', + }, + ], + [ + 'core/form-input', + { + type: 'checkbox', + name: 'export_personal_data', + label: __( 'Request data export' ), + required: false, + visibilityPermissions: 'all', + }, + ], + [ + 'core/form-input', + { + type: 'checkbox', + name: 'remove_personal_data', + label: __( 'Request data deletion' ), + required: false, + visibilityPermissions: 'all', + }, + ], + [ 'core/form-submit-button', {} ], + [ + 'core/form-input', + { + type: 'hidden', + name: 'wp-action', + value: 'wp_privacy_send_request', + }, + ], + [ + 'core/form-input', + { + type: 'hidden', + name: 'wp-privacy-request', + value: '1', + }, + ], + ], + scope: [ 'inserter', 'transform' ], + isActive: ( blockAttributes ) => + ! blockAttributes?.type || blockAttributes?.type === 'text', + }, +]; + +export default variations; diff --git a/packages/block-library/src/form/view.js b/packages/block-library/src/form/view.js new file mode 100644 index 00000000000000..05efe95da545c9 --- /dev/null +++ b/packages/block-library/src/form/view.js @@ -0,0 +1,41 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable no-undef */ +document.querySelectorAll( 'form.wp-block-form' ).forEach( function ( form ) { + // Bail If the form is not using the mailto: action. + if ( ! form.action || ! form.action.startsWith( 'mailto:' ) ) { + return; + } + + const redirectNotification = ( status ) => { + const urlParams = new URLSearchParams( window.location.search ); + urlParams.append( 'wp-form-result', status ); + window.location.search = urlParams.toString(); + }; + + // Add an event listener for the form submission. + form.addEventListener( 'submit', async function ( event ) { + event.preventDefault(); + // Get the form data and merge it with the form action and nonce. + const formData = Object.fromEntries( new FormData( form ).entries() ); + formData.formAction = form.action; + formData._ajax_nonce = wpBlockFormSettings.nonce; + formData.action = wpBlockFormSettings.action; + + try { + const response = await fetch( wpBlockFormSettings.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams( formData ).toString(), + } ); + if ( response.ok ) { + redirectNotification( 'success' ); + } else { + redirectNotification( 'error' ); + } + } catch ( error ) { + redirectNotification( 'error' ); + } + } ); +} ); diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 54aa8fca6d5f42..cba0203b477a45 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -252,10 +252,22 @@ function block_core_image_render_lightbox( $block_content, $block ) { $q->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' ); $enlarged_image_content = $q->get_updated_html(); - $background_color = esc_attr( wp_get_global_styles( array( 'color', 'background' ) ) ); + // If the current theme does NOT have a `theme.json`, or the colors are not defined, + // we need to set the background color & close button color to some default values + // because we can't get them from the Global Styles. + $background_color = '#fff'; + $close_button_color = '#000'; + if ( wp_theme_has_theme_json() ) { + $global_styles_color = wp_get_global_styles( array( 'color' ) ); + if ( ! empty( $global_styles_color['background'] ) ) { + $background_color = esc_attr( $global_styles_color['background'] ); + } + if ( ! empty( $global_styles_color['text'] ) ) { + $close_button_color = esc_attr( $global_styles_color['text'] ); + } + } $close_button_icon = ''; - $close_button_color = esc_attr( wp_get_global_styles( array( 'color', 'text' ) ) ); $dialog_label = $alt_attribute ? esc_attr( $alt_attribute ) : esc_attr__( 'Image' ); $close_button_label = esc_attr__( 'Close' ); diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index 5c3552fd80c2ee..752ff773394a44 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -169,6 +169,13 @@ outline: 5px auto -webkit-focus-ring-color; outline-offset: 5px; } + + &:hover, + &:focus, + &:not(:hover):not(:active):not(.has-background) { + background: none; + border: none; + } } } @@ -191,6 +198,13 @@ padding: 0; cursor: pointer; z-index: 5000000; + + &:hover, + &:focus, + &:not(:hover):not(:active):not(.has-background) { + background: none; + border: none; + } } .lightbox-image-container { diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 736b552bf4259b..e2e0fd9e414ef3 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -48,6 +48,10 @@ import * as cover from './cover'; import * as details from './details'; import * as embed from './embed'; import * as file from './file'; +import * as form from './form'; +import * as formInput from './form-input'; +import * as formSubmitButton from './form-submit-button'; +import * as formSubmissionNotification from './form-submission-notification'; import * as gallery from './gallery'; import * as group from './group'; import * as heading from './heading'; @@ -228,6 +232,12 @@ const getAllBlocks = () => { queryTitle, postAuthorBiography, ]; + if ( window?.__experimentalEnableFormBlocks ) { + blocks.push( form ); + blocks.push( formInput ); + blocks.push( formSubmitButton ); + blocks.push( formSubmissionNotification ); + } // When in a WordPress context, conditionally // add the classic block and TinyMCE editor diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js index 7aaf1b3ecf0eda..586ecc59432730 100644 --- a/packages/block-library/src/latest-posts/edit.js +++ b/packages/block-library/src/latest-posts/edit.js @@ -483,12 +483,17 @@ export default function LatestPostsEdit( { attributes, setAttributes } ) { .split( ' ', excerptLength ) .join( ' ' ) } { createInterpolateElement( - /* translators: excerpt truncation character, default … */ - __( ' … Read more' ), + sprintf( + /* translators: 1: The static string "Read more", 2: The post title only visible to screen readers. */ + __( '… %1$s: %2$s' ), + __( 'Read more' ), + titleTrimmed || __( '(no title)' ) + ), { a: ( // eslint-disable-next-line jsx-a11y/anchor-has-content ), + span: ( + + ), } ) } diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index 356ba5032689f7..d5f759c0c0e259 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -48,14 +48,6 @@ function render_block_core_latest_posts( $attributes ) { $block_core_latest_posts_excerpt_length = $attributes['excerptLength']; add_filter( 'excerpt_length', 'block_core_latest_posts_get_excerpt_length', 20 ); - $filter_latest_posts_excerpt_more = static function ( $more ) use ( $attributes ) { - $use_excerpt = 'excerpt' === $attributes['displayPostContentRadio']; - /* translators: %1$s is a URL to a post, excerpt truncation character, default … */ - return $use_excerpt ? sprintf( __( ' … Read more' ), esc_url( get_permalink() ) ) : $more; - }; - - add_filter( 'excerpt_more', $filter_latest_posts_excerpt_more ); - if ( ! empty( $attributes['categories'] ) ) { $args['category__in'] = array_column( $attributes['categories'], 'id' ); } @@ -151,6 +143,24 @@ function render_block_core_latest_posts( $attributes ) { $trimmed_excerpt = get_the_excerpt( $post ); + /* + * Adds a "Read more" link with screen reader text. + * […] is the default excerpt ending from wp_trim_excerpt() in Core. + */ + if ( str_ends_with( $trimmed_excerpt, ' […]' ) ) { + $excerpt_length = (int) apply_filters( 'excerpt_length', $block_core_latest_posts_excerpt_length ); + if ( $excerpt_length <= $block_core_latest_posts_excerpt_length ) { + $trimmed_excerpt = substr( $trimmed_excerpt, 0, -11 ); + $trimmed_excerpt .= sprintf( + /* translators: 1: A URL to a post, 2: The static string "Read more", 3: The post title only visible to screen readers. */ + __( '… %2$s: %3$s' ), + esc_url( $post_link ), + __( 'Read more' ), + esc_html( $title ) + ); + } + } + if ( post_password_required( $post ) ) { $trimmed_excerpt = __( 'This content is password protected.' ); } diff --git a/packages/block-library/src/preformatted/test/edit.native.js b/packages/block-library/src/preformatted/test/edit.native.js index 1fdb4532dacab6..153b1b9e9b0ab5 100644 --- a/packages/block-library/src/preformatted/test/edit.native.js +++ b/packages/block-library/src/preformatted/test/edit.native.js @@ -70,4 +70,42 @@ describe( 'Preformatted', () => { " ` ); } ); + + it( 'should split on triple Enter', async () => { + // Arrange + const screen = await initializeEditor(); + + // Act + await addBlock( screen, 'Preformatted' ); + const preformattedTextInput = await screen.findByPlaceholderText( + 'Write preformatted text…' + ); + typeInRichText( preformattedTextInput, 'Hello' ); + fireEvent( preformattedTextInput, 'onKeyDown', { + nativeEvent: {}, + preventDefault() {}, + keyCode: ENTER, + } ); + fireEvent( preformattedTextInput, 'onKeyDown', { + nativeEvent: {}, + preventDefault() {}, + keyCode: ENTER, + } ); + fireEvent( preformattedTextInput, 'onKeyDown', { + nativeEvent: {}, + preventDefault() {}, + keyCode: ENTER, + } ); + + // Assert + expect( getEditorHtml() ).toMatchInlineSnapshot( ` + " +
Hello
+ + + +

+ " + ` ); + } ); } ); diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index c4b0a37e6354d7..790e09535f4b69 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -14,6 +14,7 @@ @import "./details/style.scss"; @import "./embed/style.scss"; @import "./file/style.scss"; +@import "./form-input/style.scss"; @import "./gallery/style.scss"; @import "./group/style.scss"; @import "./heading/style.scss"; diff --git a/packages/block-library/src/verse/test/edit.native.js b/packages/block-library/src/verse/test/edit.native.js index c91c261c89c358..8794419775d325 100644 --- a/packages/block-library/src/verse/test/edit.native.js +++ b/packages/block-library/src/verse/test/edit.native.js @@ -78,4 +78,41 @@ describe( 'Verse block', () => { " ` ); } ); + + it( 'should split on triple Enter', async () => { + // Arrange + const screen = await initializeEditor(); + await addBlock( screen, 'Verse' ); + + // Act + const verseTextInput = + await screen.findByPlaceholderText( 'Write verse…' ); + typeInRichText( verseTextInput, 'Hello' ); + fireEvent( verseTextInput, 'onKeyDown', { + nativeEvent: {}, + preventDefault() {}, + keyCode: ENTER, + } ); + fireEvent( verseTextInput, 'onKeyDown', { + nativeEvent: {}, + preventDefault() {}, + keyCode: ENTER, + } ); + fireEvent( verseTextInput, 'onKeyDown', { + nativeEvent: {}, + preventDefault() {}, + keyCode: ENTER, + } ); + + // Assert + expect( getEditorHtml() ).toMatchInlineSnapshot( ` + " +
Hello
+ + + +

+ " + ` ); + } ); } ); diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 3c8176d2c29621..14166f8827ec80 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fix + +- `Modal`: fix closing when contained iframe is focused ([#51602](https://github.com/WordPress/gutenberg/pull/51602)). + ## 25.9.0 (2023-10-05) ### Enhancements @@ -13,6 +17,7 @@ - `InputControl`, `NumberControl`, `UnitControl`, `SelectControl`, `CustomSelectControl`, `TreeSelect`: Add opt-in prop for next 40px default size, superseding the `__next36pxDefaultSize` prop ([#53819](https://github.com/WordPress/gutenberg/pull/53819)). - `Modal`: add a new `size` prop to support preset widths, including a `fill` option to eventually replace the `isFullScreen` prop ([#54471](https://github.com/WordPress/gutenberg/pull/54471)). - Wrapped `TextareaControl` in a `forwardRef` call ([#54975](https://github.com/WordPress/gutenberg/pull/54975)). +- `Composite`/`AlignmentMatrixControl`/`CircularOptionPicker`: Starts the `Composite` migration from `reakit` to `ariakit` ([#54225](https://github.com/WordPress/gutenberg/pull/54225)). ### Bug Fix @@ -23,6 +28,7 @@ - `Button`: Remove `aria-selected` CSS selector from styling 'active' buttons ([#54931](https://github.com/WordPress/gutenberg/pull/54931)). - `Popover`: Apply the CSS in JS styles properly for components used within popovers. ([#54912](https://github.com/WordPress/gutenberg/pull/54912)) - `Button`: Remove hover styles when `aria-disabled` is set to `true` for the secondary variant. ([#54978](https://github.com/WordPress/gutenberg/pull/54978)) +- `Button`: Revert toggled style selector to use a class instead of attributes ([#55065](https://github.com/WordPress/gutenberg/pull/55065)). ### Internal @@ -34,6 +40,7 @@ - `SlotFill`: Migrate to TypeScript and Convert to Functional Component ``. ([#51350](https://github.com/WordPress/gutenberg/pull/51350)). - `Components`: move `ui/utils` to `utils` and remove `ui/` folder ([#54922](https://github.com/WordPress/gutenberg/pull/54922)). - Ensure `@types/` dependencies used by final type files are included in the main dependency field ([#50231](https://github.com/WordPress/gutenberg/pull/50231)). +- `Text`: Migrate to TypeScript. ([#54953](https://github.com/WordPress/gutenberg/pull/54953)). ## 25.8.0 (2023-09-20) diff --git a/packages/components/src/alignment-matrix-control/cell.tsx b/packages/components/src/alignment-matrix-control/cell.tsx index 00daa6fb316954..162ca879f1a7e5 100644 --- a/packages/components/src/alignment-matrix-control/cell.tsx +++ b/packages/components/src/alignment-matrix-control/cell.tsx @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { CompositeItem } from '../composite'; +import { CompositeItem } from '../composite/v2'; import Tooltip from '../tooltip'; import { VisuallyHidden } from '../visually-hidden'; @@ -17,6 +17,7 @@ import type { AlignmentMatrixControlCellProps } from './types'; import type { WordPressComponentProps } from '../context'; export default function Cell( { + id, isActive = false, value, ...props @@ -25,7 +26,10 @@ export default function Cell( { return ( - + } + > { /* VoiceOver needs a text content to be rendered within grid cell, otherwise it'll announce the content as "blank". So we use a visually hidden element instead of aria-label. */ } diff --git a/packages/components/src/alignment-matrix-control/index.tsx b/packages/components/src/alignment-matrix-control/index.tsx index 2d52bb0e248435..3de0e401187d53 100644 --- a/packages/components/src/alignment-matrix-control/index.tsx +++ b/packages/components/src/alignment-matrix-control/index.tsx @@ -8,32 +8,17 @@ import classnames from 'classnames'; */ import { __, isRTL } from '@wordpress/i18n'; import { useInstanceId } from '@wordpress/compose'; -import { useState, useEffect } from '@wordpress/element'; /** * Internal dependencies */ import Cell from './cell'; -import { Composite, CompositeGroup, useCompositeState } from '../composite'; +import { Composite, CompositeRow, useCompositeStore } from '../composite/v2'; import { Root, Row } from './styles/alignment-matrix-control-styles'; import AlignmentMatrixControlIcon from './icon'; -import { GRID, getItemId } from './utils'; +import { GRID, getItemId, getItemValue } from './utils'; import type { WordPressComponentProps } from '../context'; -import type { - AlignmentMatrixControlProps, - AlignmentMatrixControlValue, -} from './types'; - -const noop = () => {}; - -function useBaseId( id?: string ) { - const instanceId = useInstanceId( - AlignmentMatrixControl, - 'alignment-matrix-control' - ); - - return id || instanceId; -} +import type { AlignmentMatrixControlProps } from './types'; /** * @@ -61,31 +46,27 @@ export function AlignmentMatrixControl( { label = __( 'Alignment Matrix Control' ), defaultValue = 'center center', value, - onChange = noop, + onChange, width = 92, ...props }: WordPressComponentProps< AlignmentMatrixControlProps, 'div', false > ) { - const [ immutableDefaultValue ] = useState( value ?? defaultValue ); - const baseId = useBaseId( id ); - const initialCurrentId = getItemId( baseId, immutableDefaultValue ); + const baseId = useInstanceId( + AlignmentMatrixControl, + 'alignment-matrix-control', + id + ); - const composite = useCompositeState( { - baseId, - currentId: initialCurrentId, + const compositeStore = useCompositeStore( { + defaultActiveId: getItemId( baseId, defaultValue ), + activeId: getItemId( baseId, value ), + setActiveId: ( nextActiveId ) => { + const nextValue = getItemValue( baseId, nextActiveId ); + if ( nextValue ) onChange?.( nextValue ); + }, rtl: isRTL(), } ); - const handleOnChange = ( nextValue: AlignmentMatrixControlValue ) => { - onChange( nextValue ); - }; - - const { setCurrentId } = composite; - - useEffect( () => { - if ( typeof value !== 'undefined' ) { - setCurrentId( getItemId( baseId, value ) ); - } - }, [ value, setCurrentId, baseId ] ); + const activeId = compositeStore.useState( 'activeId' ); const classes = classnames( 'component-alignment-matrix-control', @@ -94,38 +75,34 @@ export function AlignmentMatrixControl( { return ( + } > { GRID.map( ( cells, index ) => ( - + } key={ index }> { cells.map( ( cell ) => { const cellId = getItemId( baseId, cell ); - const isActive = composite.currentId === cellId; + const isActive = cellId === activeId; return ( handleOnChange( cell ) } - tabIndex={ isActive ? 0 : -1 } /> ); } ) } - + ) ) } ); diff --git a/packages/components/src/alignment-matrix-control/stories/index.story.tsx b/packages/components/src/alignment-matrix-control/stories/index.story.tsx index 24b496d1f2432b..03bec9d92a8b78 100644 --- a/packages/components/src/alignment-matrix-control/stories/index.story.tsx +++ b/packages/components/src/alignment-matrix-control/stories/index.story.tsx @@ -6,7 +6,7 @@ import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies */ -import { useEffect, useState } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import { Icon } from '@wordpress/icons'; /** @@ -24,10 +24,11 @@ const meta: Meta< typeof AlignmentMatrixControl > = { 'AlignmentMatrixControl.Icon': AlignmentMatrixControl.Icon, }, argTypes: { - onChange: { action: 'onChange', control: { type: null } }, + onChange: { control: { type: null } }, value: { control: { type: null } }, }, parameters: { + actions: { argTypesRegex: '^on.*' }, controls: { expanded: true }, docs: { canvas: { sourceState: 'shown' } }, }, @@ -42,11 +43,6 @@ const Template: StoryFn< typeof AlignmentMatrixControl > = ( { const [ value, setValue ] = useState< AlignmentMatrixControlProps[ 'value' ] >(); - // Convenience handler for Canvas view so changes are reflected - useEffect( () => { - setValue( defaultValue ); - }, [ defaultValue ] ); - return ( { return within( getControl() ).getByRole( 'gridcell', { name } ); }; +const renderAndInitCompositeStore = async ( + jsx: JSX.Element, + focusedCell = 'center center' +) => { + const view = render( jsx ); + await waitFor( () => { + expect( getCell( focusedCell ) ).toHaveAttribute( 'tabindex', '0' ); + } ); + return view; +}; + describe( 'AlignmentMatrixControl', () => { describe( 'Basic rendering', () => { - it( 'should render', () => { - render( ); + it( 'should render', async () => { + await renderAndInitCompositeStore( ); expect( getControl() ).toBeInTheDocument(); } ); + + it( 'should be centered by default', async () => { + const user = userEvent.setup(); + + await renderAndInitCompositeStore( ); + + await user.tab(); + + expect( getCell( 'center center' ) ).toHaveFocus(); + } ); } ); - describe( 'Change value', () => { - const alignments = [ 'center left', 'center center', 'bottom center' ]; - const user = userEvent.setup(); + describe( 'Should change value', () => { + describe( 'with Mouse', () => { + describe( 'on cell click', () => { + it.each( [ + 'top left', + 'top center', + 'top right', + 'center left', + 'center right', + 'bottom left', + 'bottom center', + 'bottom right', + ] )( '%s', async ( alignment ) => { + const user = userEvent.setup(); + const spy = jest.fn(); + + await renderAndInitCompositeStore( + + ); + + const cell = getCell( alignment ); + + await user.click( cell ); - it.each( alignments )( - 'should change value on %s cell click', - async ( alignment ) => { - const spy = jest.fn(); + expect( cell ).toHaveFocus(); + expect( spy ).toHaveBeenCalledWith( alignment ); + } ); - render( - - ); + it( 'unless already focused', async () => { + const user = userEvent.setup(); + const spy = jest.fn(); - await user.click( getCell( alignment ) ); + await renderAndInitCompositeStore( + + ); - expect( getCell( alignment ) ).toHaveFocus(); + const cell = getCell( 'center center' ); - expect( spy ).toHaveBeenCalledWith( alignment ); - } - ); + await user.click( cell ); + + expect( cell ).toHaveFocus(); + expect( spy ).not.toHaveBeenCalled(); + } ); + } ); + } ); + + describe( 'with Keyboard', () => { + describe( 'on arrow press', () => { + it.each( [ + [ 'ArrowUp', 'top center' ], + [ 'ArrowLeft', 'center left' ], + [ 'ArrowDown', 'bottom center' ], + [ 'ArrowRight', 'center right' ], + ] )( '%s', async ( keyRef, cellRef ) => { + const user = userEvent.setup(); + const spy = jest.fn(); + + await renderAndInitCompositeStore( + + ); + + await user.tab(); + await user.keyboard( `[${ keyRef }]` ); + + expect( getCell( cellRef ) ).toHaveFocus(); + expect( spy ).toHaveBeenCalledWith( cellRef ); + } ); + } ); + + describe( 'but not at at edge', () => { + it.each( [ + [ 'ArrowUp', 'top left' ], + [ 'ArrowLeft', 'top left' ], + [ 'ArrowDown', 'bottom right' ], + [ 'ArrowRight', 'bottom right' ], + ] )( '%s', async ( keyRef, cellRef ) => { + const user = userEvent.setup(); + const spy = jest.fn(); + + await renderAndInitCompositeStore( + + ); + + const cell = getCell( cellRef ); + await user.click( cell ); + await user.keyboard( `[${ keyRef }]` ); + + expect( cell ).toHaveFocus(); + expect( spy ).toHaveBeenCalledWith( cellRef ); + } ); + } ); + } ); } ); } ); diff --git a/packages/components/src/alignment-matrix-control/utils.tsx b/packages/components/src/alignment-matrix-control/utils.tsx index ba5e113c42fc7f..54455b61229b01 100644 --- a/packages/components/src/alignment-matrix-control/utils.tsx +++ b/packages/components/src/alignment-matrix-control/utils.tsx @@ -2,6 +2,7 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; + /** * Internal dependencies */ @@ -31,16 +32,24 @@ export const ALIGNMENT_LABEL: Record< AlignmentMatrixControlValue, string > = { export const ALIGNMENTS = GRID.flat(); /** - * Parses and transforms an incoming value to better match the alignment values + * Normalizes and transforms an incoming value to better match the alignment values * * @param value An alignment value to parse. * * @return The parsed value. */ -export function transformValue( value: AlignmentMatrixControlValue ) { - const nextValue = value === 'center' ? 'center center' : value; +function normalize( value?: string | null ) { + const normalized = value === 'center' ? 'center center' : value; + + // Strictly speaking, this could be `string | null | undefined`, + // but will be validated shortly, so we're typecasting to an + // `AlignmentMatrixControlValue` to keep TypeScript happy. + const transformed = normalized?.replace( + '-', + ' ' + ) as AlignmentMatrixControlValue; - return nextValue.replace( '-', ' ' ) as AlignmentMatrixControlValue; + return ALIGNMENTS.includes( transformed ) ? transformed : undefined; } /** @@ -53,11 +62,25 @@ export function transformValue( value: AlignmentMatrixControlValue ) { */ export function getItemId( prefixId: string, - value: AlignmentMatrixControlValue + value?: AlignmentMatrixControlValue ) { - const valueId = transformValue( value ).replace( ' ', '-' ); + const normalized = normalize( value ); + if ( ! normalized ) return; + + const id = normalized.replace( ' ', '-' ); + return `${ prefixId }-${ id }`; +} - return `${ prefixId }-${ valueId }`; +/** + * Extracts an item value from its ID + * + * @param prefixId An ID prefix to remove + * @param id An item ID + * @return The item value + */ +export function getItemValue( prefixId: string, id?: string | null ) { + const value = id?.replace( prefixId + '-', '' ); + return normalize( value ); } /** @@ -70,8 +93,9 @@ export function getItemId( export function getAlignmentIndex( alignment: AlignmentMatrixControlValue = 'center' ) { - const item = transformValue( alignment ); - const index = ALIGNMENTS.indexOf( item ); + const normalized = normalize( alignment ); + if ( ! normalized ) return undefined; + const index = ALIGNMENTS.indexOf( normalized ); return index > -1 ? index : undefined; } diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss index e3b8c55f074e8d..03273056cfa179 100644 --- a/packages/components/src/button/style.scss +++ b/packages/components/src/button/style.scss @@ -308,8 +308,7 @@ } // Toggled style. - &[aria-pressed="true"], - &[aria-pressed="mixed"] { + &.is-pressed { color: $components-color-foreground-inverted; background: $components-color-foreground; diff --git a/packages/components/src/circular-option-picker/circular-option-picker-option.tsx b/packages/components/src/circular-option-picker/circular-option-picker-option.tsx index 2834228715115a..7dcfd635557550 100644 --- a/packages/components/src/circular-option-picker/circular-option-picker-option.tsx +++ b/packages/components/src/circular-option-picker/circular-option-picker-option.tsx @@ -8,7 +8,7 @@ import type { ForwardedRef } from 'react'; * WordPress dependencies */ import { useInstanceId } from '@wordpress/compose'; -import { forwardRef, useContext, useEffect } from '@wordpress/element'; +import { forwardRef, useContext } from '@wordpress/element'; import { Icon, check } from '@wordpress/icons'; /** @@ -16,15 +16,9 @@ import { Icon, check } from '@wordpress/icons'; */ import { CircularOptionPickerContext } from './circular-option-picker-context'; import Button from '../button'; -import { CompositeItem } from '../composite'; +import { CompositeItem } from '../composite/v2'; import Tooltip from '../tooltip'; -import type { - OptionProps, - CircularOptionPickerCompositeState, - CircularOptionPickerContextProps, -} from './types'; - -const hasSelectedOption = new Map(); +import type { OptionProps, CircularOptionPickerCompositeStore } from './types'; function UnforwardedOptionAsButton( props: { @@ -40,7 +34,7 @@ function UnforwardedOptionAsButton( { ...additionalProps } aria-pressed={ isPressed } ref={ forwardedRef } - > + /> ); } @@ -51,38 +45,29 @@ function UnforwardedOptionAsOption( id: string; className?: string; isSelected?: boolean; - context: CircularOptionPickerContextProps; + compositeStore: CircularOptionPickerCompositeStore; }, forwardedRef: ForwardedRef< any > ) { - const { id, isSelected, context, ...additionalProps } = props; - const { isComposite, ..._compositeState } = context; - const compositeState = - _compositeState as CircularOptionPickerCompositeState; - const { baseId, currentId, setCurrentId } = compositeState; + const { id, isSelected, compositeStore, ...additionalProps } = props; + const activeId = compositeStore.useState( 'activeId' ); - useEffect( () => { - // If we call `setCurrentId` here, it doesn't update for other - // Option renders in the same pass. So we have to store our own - // map to make sure that we only set the first selected option. - // We still need to check `currentId` because the control will - // update this as the user moves around, and that state should - // be maintained as the group gains and loses focus. - if ( isSelected && ! currentId && ! hasSelectedOption.get( baseId ) ) { - hasSelectedOption.set( baseId, true ); - setCurrentId( id ); - } - }, [ baseId, currentId, id, isSelected, setCurrentId ] ); + if ( isSelected && ! activeId ) { + compositeStore.setActiveId( id ); + } return ( + } + store={ compositeStore } id={ id } - role="option" - aria-selected={ !! isSelected } - ref={ forwardedRef } /> ); } @@ -96,8 +81,9 @@ export function Option( { tooltipText, ...additionalProps }: OptionProps ) { - const compositeContext = useContext( CircularOptionPickerContext ); - const { isComposite, baseId } = compositeContext; + const { baseId, compositeStore } = useContext( + CircularOptionPickerContext + ); const id = useInstanceId( Option, baseId || 'components-circular-option-picker__option' @@ -109,10 +95,10 @@ export function Option( { ...additionalProps, }; - const optionControl = isComposite ? ( + const optionControl = compositeStore ? ( ) : ( diff --git a/packages/components/src/circular-option-picker/circular-option-picker.tsx b/packages/components/src/circular-option-picker/circular-option-picker.tsx index 08c5ad94a34cb7..047b0c569c7d3e 100644 --- a/packages/components/src/circular-option-picker/circular-option-picker.tsx +++ b/packages/components/src/circular-option-picker/circular-option-picker.tsx @@ -7,14 +7,13 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useInstanceId } from '@wordpress/compose'; -import { useEffect } from '@wordpress/element'; import { isRTL } from '@wordpress/i18n'; /** * Internal dependencies */ import { CircularOptionPickerContext } from './circular-option-picker-context'; -import { Composite, useCompositeState } from '../composite'; +import { Composite, useCompositeStore } from '../composite/v2'; import type { CircularOptionPickerProps, ListboxCircularOptionPickerProps, @@ -85,30 +84,15 @@ function ListboxCircularOptionPicker( children, ...additionalProps } = props; - const rtl = isRTL(); - const compositeState = useCompositeState( { baseId, loop, rtl } ); - const { setBaseId, setLoop, setRTL } = compositeState; - - // These are necessary as `useCompositeState` is sealed after - // the first render, so although unlikely to happen, if a state - // property should change, we need to process it accordingly. - - useEffect( () => { - setBaseId( baseId ); - }, [ setBaseId, baseId ] ); - - useEffect( () => { - setLoop( loop ); - }, [ setLoop, loop ] ); - - useEffect( () => { - setRTL( rtl ); - }, [ setRTL, rtl ] ); + const compositeStore = useCompositeStore( { + focusLoop: loop, + rtl: isRTL(), + } ); const compositeContext = { - isComposite: true, - ...compositeState, + baseId, + compositeStore, }; return ( @@ -116,7 +100,8 @@ function ListboxCircularOptionPicker( { options } @@ -134,10 +119,8 @@ function ButtonsCircularOptionPicker( const { actions, options, children, baseId, ...additionalProps } = props; return ( -
- +
+ { options } { children } { actions } diff --git a/packages/components/src/circular-option-picker/types.ts b/packages/components/src/circular-option-picker/types.ts index cd9d9dca6be8df..519d81d5905107 100644 --- a/packages/components/src/circular-option-picker/types.ts +++ b/packages/components/src/circular-option-picker/types.ts @@ -14,7 +14,7 @@ import type { Icon } from '@wordpress/icons'; import type { ButtonAsButtonProps } from '../button/types'; import type { DropdownProps } from '../dropdown/types'; import type { WordPressComponentProps } from '../context'; -import type { CompositeState } from '../composite'; +import type { CompositeStore } from '../composite/v2'; type CommonCircularOptionPickerProps = { /** @@ -123,7 +123,8 @@ export type OptionProps = Omit< >; }; -export type CircularOptionPickerCompositeState = CompositeState; -export type CircularOptionPickerContextProps = - | { isComposite?: false; baseId?: string } - | ( { isComposite: true } & CircularOptionPickerCompositeState ); +export type CircularOptionPickerCompositeStore = CompositeStore; +export type CircularOptionPickerContextProps = { + baseId?: string; + compositeStore?: CircularOptionPickerCompositeStore; +}; diff --git a/packages/components/src/composite/v2.ts b/packages/components/src/composite/v2.ts new file mode 100644 index 00000000000000..d329fd3fd11dfb --- /dev/null +++ b/packages/components/src/composite/v2.ts @@ -0,0 +1,22 @@ +/** + * Composite is a component that may contain navigable items represented by + * CompositeItem. It's inspired by the WAI-ARIA Composite Role and implements + * all the keyboard navigation mechanisms to ensure that there's only one + * tab stop for the whole Composite element. This means that it can behave as + * a roving tabindex or aria-activedescendant container. + * + * @see https://ariakit.org/components/composite + */ + +/* eslint-disable-next-line no-restricted-imports */ +export { + Composite, + CompositeGroup, + CompositeGroupLabel, + CompositeItem, + CompositeRow, + useCompositeStore, +} from '@ariakit/react'; + +/* eslint-disable-next-line no-restricted-imports */ +export type { CompositeStore } from '@ariakit/react'; diff --git a/packages/components/src/heading/stories/index.story.tsx b/packages/components/src/heading/stories/index.story.tsx index e774fd53312732..d82a59f08c825b 100644 --- a/packages/components/src/heading/stories/index.story.tsx +++ b/packages/components/src/heading/stories/index.story.tsx @@ -12,7 +12,6 @@ const meta: Meta< typeof Heading > = { component: Heading, title: 'Components (Experimental)/Heading', argTypes: { - adjustLineHeightForInnerControls: { control: { type: 'text' } }, as: { control: { type: 'text' } }, color: { control: { type: 'color' } }, display: { control: { type: 'text' } }, @@ -20,9 +19,8 @@ const meta: Meta< typeof Heading > = { lineHeight: { control: { type: 'text' } }, optimizeReadabilityFor: { control: { type: 'color' } }, variant: { - control: { type: 'radio' }, - options: [ 'undefined', 'muted' ], - mapping: { undefined, muted: 'muted' }, + control: { type: 'select' }, + options: [ undefined, 'muted' ], }, weight: { control: { type: 'text' } }, }, diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index 2746c40fcaab02..041c592166ab73 100644 --- a/packages/components/src/modal/index.tsx +++ b/packages/components/src/modal/index.tsx @@ -2,7 +2,12 @@ * External dependencies */ import classnames from 'classnames'; -import type { ForwardedRef, KeyboardEvent, UIEvent } from 'react'; +import type { + ForwardedRef, + KeyboardEvent, + MutableRefObject, + UIEvent, +} from 'react'; /** * WordPress dependencies @@ -15,12 +20,13 @@ import { useState, forwardRef, useLayoutEffect, + createContext, + useContext, } from '@wordpress/element'; import { useInstanceId, useFocusReturn, useFocusOnMount, - __experimentalUseFocusOutside as useFocusOutside, useConstrainedTabbing, useMergeRefs, } from '@wordpress/compose'; @@ -36,8 +42,13 @@ import Button from '../button'; import StyleProvider from '../style-provider'; import type { ModalProps } from './types'; -// Used to count the number of open modals. -let openModalCount = 0; +// Used to track and dismiss the prior modal when another opens unless nested. +const level0Dismissers: MutableRefObject< + ModalProps[ 'onRequestClose' ] | undefined +>[] = []; +const ModalContext = createContext( level0Dismissers ); + +let isBodyOpenClassActive = false; function UnforwardedModal( props: ModalProps, @@ -91,7 +102,6 @@ function UnforwardedModal( ); const constrainedTabbingRef = useConstrainedTabbing(); const focusReturnRef = useFocusReturn(); - const focusOutsideProps = useFocusOutside( onRequestClose ); const contentRef = useRef< HTMLDivElement >( null ); const childrenContainerRef = useRef< HTMLDivElement >( null ); @@ -120,26 +130,52 @@ function UnforwardedModal( } }, [ contentRef ] ); + // Accessibly isolates/unisolates the modal. useEffect( () => { ariaHelper.modalize( ref.current ); return () => ariaHelper.unmodalize(); }, [] ); + // Keeps a fresh ref for the subsequent effect. + const refOnRequestClose = useRef< ModalProps[ 'onRequestClose' ] >(); useEffect( () => { - openModalCount++; + refOnRequestClose.current = onRequestClose; + }, [ onRequestClose ] ); - if ( openModalCount === 1 ) { - document.body.classList.add( bodyOpenClassName ); - } + // The list of `onRequestClose` callbacks of open (non-nested) Modals. Only + // one should remain open at a time and the list enables closing prior ones. + const dismissers = useContext( ModalContext ); + // Used for the tracking and dismissing any nested modals. + const nestedDismissers = useRef< typeof level0Dismissers >( [] ); + // Updates the stack tracking open modals at this level and calls + // onRequestClose for any prior and/or nested modals as applicable. + useEffect( () => { + dismissers.push( refOnRequestClose ); + const [ first, second ] = dismissers; + if ( second ) first?.current?.(); + + const nested = nestedDismissers.current; return () => { - openModalCount--; + nested[ 0 ]?.current?.(); + dismissers.shift(); + }; + }, [ dismissers ] ); - if ( openModalCount === 0 ) { + const isLevel0 = dismissers === level0Dismissers; + // Adds/removes the value of bodyOpenClassName to body element. + useEffect( () => { + if ( ! isBodyOpenClassActive ) { + isBodyOpenClassActive = true; + document.body.classList.add( bodyOpenClassName ); + } + return () => { + if ( isLevel0 && dismissers.length === 0 ) { document.body.classList.remove( bodyOpenClassName ); + isBodyOpenClassActive = false; } }; - }, [ bodyOpenClassName ] ); + }, [ bodyOpenClassName, dismissers, isLevel0 ] ); // Calls the isContentScrollable callback when the Modal children container resizes. useLayoutEffect( () => { @@ -200,12 +236,9 @@ function UnforwardedModal( onPointerUp: React.PointerEventHandler< HTMLDivElement >; } = { onPointerDown: ( event ) => { - if ( event.isPrimary && event.target === event.currentTarget ) { + if ( event.target === event.currentTarget ) { pressTarget = event.target; - // Avoids loss of focus yet also leaves `useFocusOutside` - // practically useless with its only potential trigger being - // programmatic focus movement. TODO opt for either removing - // the hook or enhancing it such that this isn't needed. + // Avoids focus changing so that focus return works as expected. event.preventDefault(); } }, @@ -222,7 +255,7 @@ function UnforwardedModal( }, }; - return createPortal( + const modal = ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
-
, +
+ ); + + return createPortal( + + { modal } + , document.body ); } diff --git a/packages/components/src/modal/test/index.tsx b/packages/components/src/modal/test/index.tsx index 69f28508c14059..9073735e94dbe9 100644 --- a/packages/components/src/modal/test/index.tsx +++ b/packages/components/src/modal/test/index.tsx @@ -167,6 +167,35 @@ describe( 'Modal', () => { expect( onRequestClose ).not.toHaveBeenCalled(); } ); + it( 'should request closing of nested modal when outer modal unmounts', async () => { + const user = userEvent.setup(); + const onRequestClose = jest.fn(); + + const RequestCloseOfNested = () => { + const [ isShown, setIsShown ] = useState( true ); + return ( + <> + { isShown && ( + { + if ( key === 'o' ) setIsShown( false ); + } } + onRequestClose={ noop } + > + +

Nested modal content

+
+
+ ) } + + ); + }; + render( ); + + await user.keyboard( 'o' ); + expect( onRequestClose ).toHaveBeenCalled(); + } ); + it( 'should accessibly hide and show siblings including outer modals', async () => { const user = userEvent.setup(); diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index 38707860e1ce91..fd61c2564e6b06 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -6,6 +6,13 @@ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/pri /** * Internal dependencies */ +import { + Composite as CompositeV2, + CompositeGroup as CompositeGroupV2, + CompositeItem as CompositeItemV2, + CompositeRow as CompositeRowV2, + useCompositeStore as useCompositeStoreV2, +} from './composite/v2'; import { default as CustomSelectControl } from './custom-select-control'; import { positionToPlacement as __experimentalPopoverLegacyPositionToPlacement } from './popover/utils'; import { default as ProgressBar } from './progress-bar'; @@ -33,6 +40,11 @@ export const { lock, unlock } = export const privateApis = {}; lock( privateApis, { + CompositeV2, + CompositeGroupV2, + CompositeItemV2, + CompositeRowV2, + useCompositeStoreV2, CustomSelectControl, __experimentalPopoverLegacyPositionToPlacement, createPrivateSlotFill, diff --git a/packages/components/src/text/README.md b/packages/components/src/text/README.md index 7747ec9cbc7277..6b1fc156158407 100644 --- a/packages/components/src/text/README.md +++ b/packages/components/src/text/README.md @@ -22,7 +22,7 @@ function Example() { ### adjustLineHeightForInnerControls -**Type**: `boolean`,`"large"`,`"medium"`,`"small"`,`"xSmall"` +**Type**: `"large"`,`"medium"`,`"small"`,`"xSmall"` Automatically calculate the appropriate line-height value for contents that render text and Control elements (e.g. `TextInput`). @@ -31,7 +31,7 @@ import { __experimentalText as Text, TextInput } from '@wordpress/components'; function Example() { return ( - + Lorem ipsum dolor sit amet, consectetur diff --git a/packages/components/src/text/component.js b/packages/components/src/text/component.tsx similarity index 64% rename from packages/components/src/text/component.js rename to packages/components/src/text/component.tsx index f1ce842dae9153..f3fe69d936584c 100644 --- a/packages/components/src/text/component.js +++ b/packages/components/src/text/component.tsx @@ -1,15 +1,20 @@ /** * Internal dependencies */ +import type { WordPressComponentProps } from '../context'; import { contextConnect } from '../context'; import { View } from '../view'; import useText from './hook'; +import type { Props } from './types'; /** - * @param {import('../context').WordPressComponentProps} props - * @param {import('react').ForwardedRef} forwardedRef + * @param props + * @param forwardedRef */ -function Text( props, forwardedRef ) { +function UnconnectedText( + props: WordPressComponentProps< Props, 'span' >, + forwardedRef: React.ForwardedRef< any > +) { const textProps = useText( props ); return ; @@ -31,6 +36,5 @@ function Text( props, forwardedRef ) { * } * ``` */ -const ConnectedText = contextConnect( Text, 'Text' ); - -export default ConnectedText; +export const Text = contextConnect( UnconnectedText, 'Text' ); +export default Text; diff --git a/packages/components/src/text/hook.js b/packages/components/src/text/hook.ts similarity index 87% rename from packages/components/src/text/hook.js rename to packages/components/src/text/hook.ts index 5198845c1dae78..a447b2ce5133be 100644 --- a/packages/components/src/text/hook.js +++ b/packages/components/src/text/hook.ts @@ -1,6 +1,7 @@ /** * External dependencies */ +import type { SerializedStyles } from '@emotion/react'; import { css } from '@emotion/react'; /** @@ -11,6 +12,7 @@ import { useMemo, Children, cloneElement } from '@wordpress/element'; /** * Internal dependencies */ +import type { WordPressComponentProps } from '../context'; import { hasConnectNamespace, useContextSystem } from '../context'; import { useTruncate } from '../truncate'; import { getOptimalTextShade } from '../utils/colors'; @@ -20,11 +22,15 @@ import { getFontSize } from '../utils/font-size'; import { CONFIG, COLORS } from '../utils'; import { getLineHeight } from './get-line-height'; import { useCx } from '../utils/hooks/use-cx'; +import type { Props } from './types'; +import type React from 'react'; /** * @param {import('../context').WordPressComponentProps} props */ -export default function useText( props ) { +export default function useText( + props: WordPressComponentProps< Props, 'span' > +) { const { adjustLineHeightForInnerControls, align, @@ -50,8 +56,7 @@ export default function useText( props ) { ...otherProps } = useContextSystem( props, 'Text' ); - /** @type {import('react').ReactNode} */ - let content = children; + let content: React.ReactNode = children; const isHighlighter = Array.isArray( highlightWords ); const isCaption = size === 'caption'; @@ -64,9 +69,7 @@ export default function useText( props ) { content = createHighlighterText( { autoEscape: highlightEscape, - // Disable reason: We need to disable this otherwise it erases the cast - // eslint-disable-next-line object-shorthand - children: /** @type {string} */ ( children ), + children, caseSensitive: highlightCaseSensitive, searchWords: highlightWords, sanitize: highlightSanitize, @@ -76,7 +79,7 @@ export default function useText( props ) { const cx = useCx(); const classes = useMemo( () => { - const sx = {}; + const sx: Record< string, SerializedStyles | null > = {}; const lineHeight = getLineHeight( adjustLineHeightForInnerControls, @@ -87,12 +90,7 @@ export default function useText( props ) { color, display, fontSize: getFontSize( size ), - /* eslint-disable jsdoc/valid-types */ - fontWeight: - /** @type {import('react').CSSProperties['fontWeight']} */ ( - weight - ), - /* eslint-enable jsdoc/valid-types */ + fontWeight: weight, lineHeight, letterSpacing, textAlign: align, @@ -143,8 +141,7 @@ export default function useText( props ) { weight, ] ); - /** @type {undefined | 'auto' | 'none'} */ - let finalEllipsizeMode; + let finalEllipsizeMode: undefined | 'auto' | 'none'; if ( truncate === true ) { finalEllipsizeMode = 'auto'; } diff --git a/packages/components/src/text/index.js b/packages/components/src/text/index.ts similarity index 100% rename from packages/components/src/text/index.js rename to packages/components/src/text/index.ts diff --git a/packages/components/src/text/stories/index.story.js b/packages/components/src/text/stories/index.story.js deleted file mode 100644 index b1b4e3f455536b..00000000000000 --- a/packages/components/src/text/stories/index.story.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Internal dependencies - */ -import { Text } from '..'; - -export default { - component: Text, - title: 'Components (Experimental)/Text', -}; - -export const _default = () => { - return Hello; -}; - -export const Truncate = () => { - return ( - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut - facilisis dictum tortor, eu tincidunt justo scelerisque tincidunt. - Duis semper dui id augue malesuada, ut feugiat nisi aliquam. - Vestibulum venenatis diam sem, finibus dictum massa semper in. Nulla - facilisi. Nunc vulputate faucibus diam, in lobortis arcu ornare vel. - In dignissim nunc sed facilisis finibus. Etiam imperdiet mattis - arcu, sed rutrum sapien blandit gravida. Aenean sollicitudin neque - eget enim blandit, sit amet rutrum leo vehicula. Nunc malesuada - ultricies eros ut faucibus. Aliquam erat volutpat. Nulla nec feugiat - risus. Vivamus iaculis dui aliquet ante ultricies feugiat. - Vestibulum ante ipsum primis in faucibus orci luctus et ultrices - posuere cubilia curae; Vivamus nec pretium velit, sit amet - consectetur ante. Praesent porttitor ex eget fermentum mattis. - - ); -}; - -export const Highlight = () => { - return ( - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut - facilisis dictum tortor, eu tincidunt justo scelerisque tincidunt. - Duis semper dui id augue malesuada, ut feugiat nisi aliquam. - Vestibulum venenatis diam sem, finibus dictum massa semper in. Nulla - facilisi. Nunc vulputate faucibus diam, in lobortis arcu ornare vel. - In dignissim nunc sed facilisis finibus. Etiam imperdiet mattis - arcu, sed rutrum sapien blandit gravida. Aenean sollicitudin neque - eget enim blandit, sit amet rutrum leo vehicula. Nunc malesuada - ultricies eros ut faucibus. Aliquam erat volutpat. Nulla nec feugiat - risus. Vivamus iaculis dui aliquet ante ultricies feugiat. - Vestibulum ante ipsum primis in faucibus orci luctus et ultrices - posuere cubilia curae; Vivamus nec pretium velit, sit amet - consectetur ante. Praesent porttitor ex eget fermentum mattis. - - ); -}; diff --git a/packages/components/src/text/stories/index.story.tsx b/packages/components/src/text/stories/index.story.tsx new file mode 100644 index 00000000000000..f762ca3b4e3ff7 --- /dev/null +++ b/packages/components/src/text/stories/index.story.tsx @@ -0,0 +1,80 @@ +/** + * External dependencies + */ +import type { Meta, StoryFn } from '@storybook/react'; + +/** + * Internal dependencies + */ +import { Text } from '../component'; + +const meta: Meta< typeof Text > = { + component: Text, + title: 'Components (Experimental)/Text', + argTypes: { + as: { control: { type: 'text' } }, + color: { control: { type: 'color' } }, + display: { control: { type: 'text' } }, + lineHeight: { control: { type: 'text' } }, + letterSpacing: { control: { type: 'text' } }, + optimizeReadabilityFor: { control: { type: 'color' } }, + size: { control: { type: 'text' } }, + variant: { + options: [ undefined, 'muted' ], + control: { type: 'select' }, + }, + weight: { control: { type: 'text' } }, + }, + parameters: { + actions: { argTypesRegex: '^on.*' }, + controls: { expanded: true }, + docs: { canvas: { sourceState: 'shown' } }, + }, +}; +export default meta; + +const Template: StoryFn< typeof Text > = ( props ) => { + return ; +}; + +export const Default = Template.bind( {} ); +Default.args = { + children: 'Code is poetry', +}; + +export const Truncate = Template.bind( {} ); +Truncate.args = { + children: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut +facilisis dictum tortor, eu tincidunt justo scelerisque tincidunt. +Duis semper dui id augue malesuada, ut feugiat nisi aliquam. +Vestibulum venenatis diam sem, finibus dictum massa semper in. Nulla +facilisi. Nunc vulputate faucibus diam, in lobortis arcu ornare vel. +In dignissim nunc sed facilisis finibus. Etiam imperdiet mattis +arcu, sed rutrum sapien blandit gravida. Aenean sollicitudin neque +eget enim blandit, sit amet rutrum leo vehicula. Nunc malesuada +ultricies eros ut faucibus. Aliquam erat volutpat. Nulla nec feugiat +risus. Vivamus iaculis dui aliquet ante ultricies feugiat. +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices +posuere cubilia curae; Vivamus nec pretium velit, sit amet +consectetur ante. Praesent porttitor ex eget fermentum mattis.`, + numberOfLines: 2, + truncate: true, +}; + +export const Highlight = Template.bind( {} ); +Highlight.args = { + children: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut +facilisis dictum tortor, eu tincidunt justo scelerisque tincidunt. +Duis semper dui id augue malesuada, ut feugiat nisi aliquam. +Vestibulum venenatis diam sem, finibus dictum massa semper in. Nulla +facilisi. Nunc vulputate faucibus diam, in lobortis arcu ornare vel. +In dignissim nunc sed facilisis finibus. Etiam imperdiet mattis +arcu, sed rutrum sapien blandit gravida. Aenean sollicitudin neque +eget enim blandit, sit amet rutrum leo vehicula. Nunc malesuada +ultricies eros ut faucibus. Aliquam erat volutpat. Nulla nec feugiat +risus. Vivamus iaculis dui aliquet ante ultricies feugiat. +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices +posuere cubilia curae; Vivamus nec pretium velit, sit amet +consectetur ante. Praesent porttitor ex eget fermentum mattis.`, + highlightWords: [ 'con' ], +}; diff --git a/packages/components/src/text/styles.js b/packages/components/src/text/styles.ts similarity index 100% rename from packages/components/src/text/styles.js rename to packages/components/src/text/styles.ts diff --git a/packages/components/src/text/types.ts b/packages/components/src/text/types.ts index df60f4ee0e2d7b..e4702c1f3257dd 100644 --- a/packages/components/src/text/types.ts +++ b/packages/components/src/text/types.ts @@ -29,12 +29,7 @@ export interface Props extends TruncateProps { /** * Automatically calculate the appropriate line-height value for contents that render text and Control elements (e.g. `TextInput`). */ - adjustLineHeightForInnerControls?: - | boolean - | 'large' - | 'medium' - | 'small' - | 'xSmall'; + adjustLineHeightForInnerControls?: 'large' | 'medium' | 'small' | 'xSmall'; /** * Adjusts the text color. */ diff --git a/packages/components/src/text/utils.js b/packages/components/src/text/utils.ts similarity index 76% rename from packages/components/src/text/utils.js rename to packages/components/src/text/utils.ts index 2496c86cca25ae..85e41a56c6e349 100644 --- a/packages/components/src/text/utils.js +++ b/packages/components/src/text/utils.ts @@ -2,6 +2,7 @@ * External dependencies */ import memoize from 'memize'; +import type { FindAllArgs } from 'highlight-words-core'; import { findAll } from 'highlight-words-core'; /** @@ -14,7 +15,6 @@ import { createElement } from '@wordpress/element'; * https://github.com/bvaughn/react-highlight-words/blob/HEAD/src/Highlighter.js */ -/* eslint-disable jsdoc/valid-types */ /** * @typedef Options * @property {string} [activeClassName=''] Classname for active highlighted areas. @@ -33,28 +33,55 @@ import { createElement } from '@wordpress/element'; * @property {import('react').AllHTMLAttributes['style']} [unhighlightStyle] Style to apply to unhighlighted text. */ +interface Options { + activeClassName?: string; + activeIndex?: number; + activeStyle?: React.AllHTMLAttributes< HTMLDivElement >[ 'style' ]; + autoEscape?: boolean; + caseSensitive?: boolean; + children: string; + findChunks?: FindAllArgs[ 'findChunks' ]; + highlightClassName?: string | Record< string, unknown >; + highlightStyle?: React.AllHTMLAttributes< HTMLDivElement >[ 'style' ]; + highlightTag?: keyof JSX.IntrinsicElements; + sanitize?: FindAllArgs[ 'sanitize' ]; + searchWords?: string[]; + unhighlightClassName?: string; + unhighlightStyle?: React.AllHTMLAttributes< HTMLDivElement >[ 'style' ]; +} + /** * Maps props to lowercase names. * - * @template {Record} T - * @param {T} object Props to map. - * @return {{[K in keyof T as Lowercase]: T[K]}} The mapped props. + * @param object Props to map. + * @return The mapped props. */ -/* eslint-enable jsdoc/valid-types */ -const lowercaseProps = ( object ) => { - /** @type {any} */ - const mapped = {}; +const lowercaseProps = < T extends Record< string, unknown > >( object: T ) => { + const mapped: Record< string, unknown > = {}; for ( const key in object ) { mapped[ key.toLowerCase() ] = object[ key ]; } - return mapped; + return mapped as { [ K in keyof T as Lowercase< string & K > ]: T[ K ] }; }; const memoizedLowercaseProps = memoize( lowercaseProps ); /** - * - * @param {Options} options + * @param options + * @param options.activeClassName + * @param options.activeIndex + * @param options.activeStyle + * @param options.autoEscape + * @param options.caseSensitive + * @param options.children + * @param options.findChunks + * @param options.highlightClassName + * @param options.highlightStyle + * @param options.highlightTag + * @param options.sanitize + * @param options.searchWords + * @param options.unhighlightClassName + * @param options.unhighlightStyle */ export function createHighlighterText( { activeClassName = '', @@ -71,7 +98,7 @@ export function createHighlighterText( { searchWords = [], unhighlightClassName = '', unhighlightStyle, -} ) { +}: Options ) { if ( ! children ) return null; if ( typeof children !== 'string' ) return children; @@ -122,8 +149,7 @@ export function createHighlighterText( { ? Object.assign( {}, highlightStyle, activeStyle ) : highlightStyle; - /** @type {Record} */ - const props = { + const props: Record< string, unknown > = { children: text, className: highlightClassNames, key: index, diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index fde8cdba93717d..3fb3af96eb9ad0 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -223,6 +223,15 @@ export const rootEntitiesConfig = [ baseURLParams: { context: 'edit' }, key: 'plugin', }, + { + label: __( 'Post status' ), + name: 'status', + kind: 'root', + baseURL: '/wp/v2/statuses', + baseURLParams: { context: 'edit' }, + plural: 'postStatuses', + key: 'slug', + }, ]; export const additionalEntityConfigLoaders = [ diff --git a/packages/e2e-test-utils-playwright/src/admin/create-new-post.js b/packages/e2e-test-utils-playwright/src/admin/create-new-post.js index ef077e8e935e39..81822e2514a731 100644 --- a/packages/e2e-test-utils-playwright/src/admin/create-new-post.js +++ b/packages/e2e-test-utils-playwright/src/admin/create-new-post.js @@ -30,20 +30,10 @@ export async function createNewPost( { await this.visitAdminPage( 'post-new.php', query ); - // Wait for both iframed and non-iframed canvas and resolve once the - // currently available one is ready. To make this work, we need an inner - // legacy canvas selector that is unavailable directly when the canvas is - // iframed. - await Promise.any( [ - this.page.locator( '.wp-block-post-content' ).waitFor(), - this.page - .frameLocator( '[name=editor-canvas]' ) - .locator( 'body > *' ) - .first() - .waitFor(), - ] ); - - await this.page.evaluate( ( welcomeGuide ) => { + await this.page.waitForFunction( ( welcomeGuide ) => { + if ( ! window?.wp?.data?.dispatch ) { + return false; + } window.wp.data .dispatch( 'core/preferences' ) .set( 'core/edit-post', 'welcomeGuide', welcomeGuide ); @@ -51,5 +41,7 @@ export async function createNewPost( { window.wp.data .dispatch( 'core/preferences' ) .set( 'core/edit-post', 'fullscreenMode', false ); + + return true; }, showWelcomeGuide ); } diff --git a/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts b/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts index f5adb295a00a55..858c9da980fc13 100644 --- a/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts +++ b/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts @@ -19,6 +19,10 @@ type Block = { * @return The blocks. */ export async function getBlocks( this: Editor, { full = false } = {} ) { + await this.page.waitForFunction( + () => window?.wp?.blocks && window?.wp?.data + ); + return await this.page.evaluate( ( [ _full ] ) => { // Remove other unpredictable properties like clientId from blocks for testing purposes. diff --git a/packages/e2e-test-utils-playwright/src/editor/get-edited-post-content.ts b/packages/e2e-test-utils-playwright/src/editor/get-edited-post-content.ts index c294cd7d1b9399..a0f14d70a2e83e 100644 --- a/packages/e2e-test-utils-playwright/src/editor/get-edited-post-content.ts +++ b/packages/e2e-test-utils-playwright/src/editor/get-edited-post-content.ts @@ -11,6 +11,8 @@ import type { Editor } from './index'; * @return Promise resolving with post content markup. */ export async function getEditedPostContent( this: Editor ) { + await this.page.waitForFunction( () => window?.wp?.data ); + return await this.page.evaluate( () => window.wp.data.select( 'core/editor' ).getEditedPostContent() ); diff --git a/packages/e2e-test-utils-playwright/src/editor/index.ts b/packages/e2e-test-utils-playwright/src/editor/index.ts index 395fbb4f98b696..673149d4e69e0c 100644 --- a/packages/e2e-test-utils-playwright/src/editor/index.ts +++ b/packages/e2e-test-utils-playwright/src/editor/index.ts @@ -1,7 +1,12 @@ /** * External dependencies */ -import type { Browser, Page, BrowserContext, Frame } from '@playwright/test'; +import type { + Browser, + Page, + BrowserContext, + FrameLocator, +} from '@playwright/test'; /** * Internal dependencies @@ -19,6 +24,7 @@ import { setContent } from './set-content'; import { showBlockToolbar } from './show-block-toolbar'; import { saveSiteEditorEntities } from './site-editor'; import { setIsFixedToolbar } from './set-is-fixed-toolbar'; +import { switchToLegacyCanvas } from './switch-to-legacy-canvas'; import { transformBlockTo } from './transform-block-to'; type EditorConstructorProps = { @@ -36,8 +42,8 @@ export class Editor { this.browser = this.context.browser()!; } - get canvas(): Frame | Page { - return this.page.frame( 'editor-canvas' ) || this.page; + get canvas(): FrameLocator { + return this.page.frameLocator( '[name="editor-canvas"]' ); } /** @borrows clickBlockOptionsMenuItem as this.clickBlockOptionsMenuItem */ @@ -72,6 +78,9 @@ export class Editor { /** @borrows setIsFixedToolbar as this.setIsFixedToolbar */ setIsFixedToolbar: typeof setIsFixedToolbar = setIsFixedToolbar.bind( this ); + /** @borrows switchToLegacyCanvas as this.switchToLegacyCanvas */ + switchToLegacyCanvas: typeof switchToLegacyCanvas = + switchToLegacyCanvas.bind( this ); /** @borrows transformBlockTo as this.transformBlockTo */ transformBlockTo: typeof transformBlockTo = transformBlockTo.bind( this ); } diff --git a/packages/e2e-test-utils-playwright/src/editor/insert-block.ts b/packages/e2e-test-utils-playwright/src/editor/insert-block.ts index 646bcd1b410a11..ef923cf667d37f 100644 --- a/packages/e2e-test-utils-playwright/src/editor/insert-block.ts +++ b/packages/e2e-test-utils-playwright/src/editor/insert-block.ts @@ -19,6 +19,10 @@ async function insertBlock( this: Editor, blockRepresentation: BlockRepresentation ) { + await this.page.waitForFunction( + () => window?.wp?.blocks && window?.wp?.data + ); + await this.page.evaluate( ( _blockRepresentation ) => { function recursiveCreateBlock( { name, diff --git a/packages/e2e-test-utils-playwright/src/editor/set-content.ts b/packages/e2e-test-utils-playwright/src/editor/set-content.ts index ff64a81db4d6c8..d44d22f74d049d 100644 --- a/packages/e2e-test-utils-playwright/src/editor/set-content.ts +++ b/packages/e2e-test-utils-playwright/src/editor/set-content.ts @@ -10,6 +10,10 @@ import type { Editor } from './index'; * @param html Serialized block HTML. */ async function setContent( this: Editor, html: string ) { + await this.page.waitForFunction( + () => window?.wp?.blocks && window?.wp?.data + ); + await this.page.evaluate( ( _html ) => { const blocks = window.wp.blocks.parse( _html ); diff --git a/packages/e2e-test-utils-playwright/src/editor/set-is-fixed-toolbar.ts b/packages/e2e-test-utils-playwright/src/editor/set-is-fixed-toolbar.ts index 0e642a1de76626..93c2cd14e1ebc5 100644 --- a/packages/e2e-test-utils-playwright/src/editor/set-is-fixed-toolbar.ts +++ b/packages/e2e-test-utils-playwright/src/editor/set-is-fixed-toolbar.ts @@ -10,6 +10,8 @@ import type { Editor } from './index'; * @param isFixed Boolean value true/false for on/off. */ export async function setIsFixedToolbar( this: Editor, isFixed: boolean ) { + await this.page.waitForFunction( () => window?.wp?.data ); + await this.page.evaluate( ( _isFixed ) => { window.wp.data .dispatch( 'core/preferences' ) diff --git a/packages/e2e-test-utils-playwright/src/editor/switch-to-legacy-canvas.ts b/packages/e2e-test-utils-playwright/src/editor/switch-to-legacy-canvas.ts new file mode 100644 index 00000000000000..b7c4c84487dbbf --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/editor/switch-to-legacy-canvas.ts @@ -0,0 +1,20 @@ +/** + * Internal dependencies + */ +import type { Editor } from './index'; + +/** + * Switches to legacy (non-iframed) canvas. + * + * @param this + */ +export async function switchToLegacyCanvas( this: Editor ) { + await this.page.waitForFunction( () => window?.wp?.blocks ); + + await this.page.evaluate( () => { + window.wp.blocks.registerBlockType( 'test/v2', { + apiVersion: '2', + title: 'test', + } ); + } ); +} diff --git a/packages/e2e-test-utils-playwright/src/editor/transform-block-to.ts b/packages/e2e-test-utils-playwright/src/editor/transform-block-to.ts index 9701eb70ec65fb..75102983069d4a 100644 --- a/packages/e2e-test-utils-playwright/src/editor/transform-block-to.ts +++ b/packages/e2e-test-utils-playwright/src/editor/transform-block-to.ts @@ -10,6 +10,10 @@ import type { Editor } from './index'; * @param name Block name. */ export async function transformBlockTo( this: Editor, name: string ) { + await this.page.waitForFunction( + () => window?.wp?.blocks && window?.wp?.data + ); + await this.page.evaluate( ( [ blockName ] ) => { const clientIds = window.wp.data diff --git a/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts b/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts index f6378b73e33ecc..fa43fc76d27c33 100644 --- a/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts +++ b/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts @@ -54,29 +54,6 @@ async function dragFiles( } ) ); - const dataTransfer = await this.page.evaluateHandle( - async ( _fileObjects ) => { - const dt = new DataTransfer(); - const fileInstances = await Promise.all( - _fileObjects.map( async ( fileObject ) => { - const blob = await fetch( - `data:${ fileObject.mimeType };base64,${ fileObject.base64 }` - ).then( ( res ) => res.blob() ); - return new File( [ blob ], fileObject.name, { - type: fileObject.mimeType ?? undefined, - } ); - } ) - ); - - fileInstances.forEach( ( file ) => { - dt.items.add( file ); - } ); - - return dt; - }, - fileObjects - ); - // CDP doesn't actually support dragging files, this is only a _good enough_ // dummy data so that it will correctly send the relevant events. const dragData = { @@ -159,6 +136,29 @@ async function dragFiles( throw new Error( 'Element not found.' ); } + const dataTransfer = await locator.evaluateHandle( + async ( _node, _fileObjects ) => { + const dt = new DataTransfer(); + const fileInstances = await Promise.all( + _fileObjects.map( async ( fileObject: any ) => { + const blob = await fetch( + `data:${ fileObject.mimeType };base64,${ fileObject.base64 }` + ).then( ( res ) => res.blob() ); + return new File( [ blob ], fileObject.name, { + type: fileObject.mimeType ?? undefined, + } ); + } ) + ); + + fileInstances.forEach( ( file ) => { + dt.items.add( file ); + } ); + + return dt; + }, + fileObjects + ); + await locator.dispatchEvent( 'drop', { dataTransfer } ); await cdpSession.detach(); diff --git a/packages/e2e-tests/specs/local/demo.test.js b/packages/e2e-tests/specs/local/demo.test.js deleted file mode 100644 index f3a1433d720888..00000000000000 --- a/packages/e2e-tests/specs/local/demo.test.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createEmbeddingMatcher, - createJSONResponse, - createNewPost, - setUpResponseMocking, - visitAdminPage, -} from '@wordpress/e2e-test-utils'; - -const MOCK_VIMEO_RESPONSE = { - url: 'https://vimeo.com/22439234', - html: '', - type: 'video', - provider_name: 'Vimeo', - provider_url: 'https://vimeo.com', - version: '1.0', -}; - -describe( 'new editor state', () => { - beforeAll( async () => { - // First, make sure that the block editor is properly configured. - await createNewPost(); - - await setUpResponseMocking( [ - { - match: createEmbeddingMatcher( 'https://vimeo.com/22439234' ), - onRequestMatch: createJSONResponse( MOCK_VIMEO_RESPONSE ), - }, - ] ); - - await Promise.all( [ - visitAdminPage( 'post-new.php', 'gutenberg-demo' ), - page.waitForNavigation( { waitUntil: 'networkidle0' } ), - ] ); - } ); - - it( 'content should load, making the post dirty', async () => { - const isDirty = await page.evaluate( () => { - const { select } = window.wp.data; - return select( 'core/editor' ).isEditedPostDirty(); - } ); - expect( isDirty ).toBeTruthy(); - - await page.waitForSelector( 'button.editor-post-save-draft' ); - - expect( await page.$( 'button.editor-post-save-draft' ) ).toBeTruthy(); - } ); -} ); diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index e7b8f913efe7ca..b767ad77adc6d9 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -22,6 +22,7 @@ import PageActions from '../page-actions'; import { DataViews, PAGE_SIZE_VALUES } from '../dataviews'; const EMPTY_ARRAY = []; +const EMPTY_OBJECT = {}; export default function PagePages() { const [ reset, setResetQuery ] = useState( ( v ) => ! v ); @@ -32,12 +33,14 @@ export default function PagePages() { pageSize: PAGE_SIZE_VALUES[ 0 ], } ); // Request post statuses to get the proper labels. - const [ postStatuses, setPostStatuses ] = useState( EMPTY_ARRAY ); - useEffect( () => { - apiFetch( { - path: '/wp/v2/statuses', - } ).then( setPostStatuses ); - }, [] ); + const { records: statuses } = useEntityRecords( 'root', 'status' ); + const postStatuses = + statuses === null + ? EMPTY_OBJECT + : statuses.reduce( ( acc, status ) => { + acc[ status.slug ] = status.name; + return acc; + }, EMPTY_OBJECT ); // TODO: probably memo other objects passed as state(ex:https://tanstack.com/table/v8/docs/examples/react/pagination-controlled). const pagination = useMemo( @@ -66,7 +69,7 @@ export default function PagePages() { reset, ] ); - const { records, isResolving: isLoading } = useEntityRecords( + const { records: pages, isResolving: isLoadingPages } = useEntityRecords( 'postType', 'page', queryArgs @@ -136,7 +139,8 @@ export default function PagePages() { header: 'Status', id: 'status', cell: ( props ) => - postStatuses[ props.row.original.status ]?.name, + postStatuses[ props.row.original.status ] ?? + props.row.original.status, }, { header: { __( 'Actions' ) }, @@ -161,8 +165,8 @@ export default function PagePages() { { + if ( ! id ) { + return undefined; + } + + const editedRecord = select( coreStore ).getEditedEntityRecord( + 'postType', + NAVIGATION_POST_TYPE, + id + ); + + // Do not display a 'trashed' navigation menu. + return editedRecord.status === 'trash' + ? undefined + : editedRecord.title; + }, + [ id ] + ); +} diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 9316c4693cfaa6..600f855ba0ec11 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,7 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased +- [*] Exit Preformatted and Verse blocks by triple pressing the Return key [#53354] ## 1.105.0 - [*] Limit inner blocks nesting depth to avoid call stack size exceeded crash [#54382] diff --git a/packages/react-native-editor/metro.config.js b/packages/react-native-editor/metro.config.js index 05e57e3cfbcabd..307853a612c8cf 100644 --- a/packages/react-native-editor/metro.config.js +++ b/packages/react-native-editor/metro.config.js @@ -27,6 +27,7 @@ module.exports = { inlineRequires: false, }, } ), + unstable_allowRequireContext: true, // Used for optional setup configuration. }, server: { enhanceMiddleware: ( middleware ) => ( req, res, next ) => { diff --git a/packages/react-native-editor/src/index.js b/packages/react-native-editor/src/index.js index a4ca31e3e94888..32ba1c6f3441d0 100644 --- a/packages/react-native-editor/src/index.js +++ b/packages/react-native-editor/src/index.js @@ -51,6 +51,12 @@ const registerGutenberg = ( { // Initialize editor this.editorComponent = setup(); + // Apply optional setup configuration, enabling modification via hooks. + if ( typeof require.context === 'function' ) { + const req = require.context( './', false, /setup-local\.js$/ ); + req.keys().forEach( ( key ) => req( key ).default() ); + } + // Dispatch pre-render hooks. doAction( 'native.pre-render', parentProps ); diff --git a/phpunit/block-supports/elements-test.php b/phpunit/block-supports/elements-test.php new file mode 100644 index 00000000000000..efea11887b620b --- /dev/null +++ b/phpunit/block-supports/elements-test.php @@ -0,0 +1,305 @@ +test_block_name = null; + } + + public function tear_down() { + WP_Style_Engine_CSS_Rules_Store_Gutenberg::remove_all_stores(); + unregister_block_type( $this->test_block_name ); + $this->test_block_name = null; + parent::tear_down(); + } + + /** + * Registers a test block type with the provided color block supports. + * + * @param array $color_settings The color block support settings used for elements. + */ + public function register_block_with_color_settings( $color_settings ) { + $this->test_block_name = 'test/element-block-supports'; + + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'color' => $color_settings, + ), + ) + ); + } + + /** + * Tests that elements block support applies the correct classname. + * + * @covers ::gutenberg_render_elements_support + * + * @dataProvider data_elements_block_support_class + * + * @param array $color_settings The color block support settings used for elements support. + * @param array $elements_styles The elements styles within the block attributes. + * @param string $block_markup Original block markup. + * @param string $expected_markup Resulting markup after application of elements block support. + */ + public function test_elements_block_support_class( $color_settings, $elements_styles, $block_markup, $expected_markup ) { + $this->register_block_with_color_settings( $color_settings ); + + $block = array( + 'blockName' => $this->test_block_name, + 'attrs' => array( + 'style' => array( + 'elements' => $elements_styles, + ), + ), + ); + + $actual = gutenberg_render_elements_support( $block_markup, $block ); + + $this->assertMatchesRegularExpression( + $expected_markup, + $actual, + 'Elements block wrapper markup should be correct' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_elements_block_support_class() { + $color_styles = array( + 'text' => 'var:preset|color|vivid-red', + 'background' => '#fff', + ); + + return array( + 'button element styles with serialization skipped' => array( + 'color_settings' => array( + 'button' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'button' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

Hello WordPress!

', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + ), + 'link element styles with serialization skipped' => array( + 'color_settings' => array( + 'link' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'link' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

Hello WordPress!

', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + ), + 'heading element styles with serialization skipped' => array( + 'color_settings' => array( + 'heading' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'heading' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

Hello WordPress!

', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + ), + 'button element styles apply class to wrapper' => array( + 'color_settings' => array( 'button' => true ), + 'elements_styles' => array( + 'button' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

Hello WordPress!

', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + ), + 'link element styles apply class to wrapper' => array( + 'color_settings' => array( 'link' => true ), + 'elements_styles' => array( + 'link' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

Hello WordPress!

', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + ), + 'heading element styles apply class to wrapper' => array( + 'color_settings' => array( 'heading' => true ), + 'elements_styles' => array( + 'heading' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

Hello WordPress!

', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + ), + 'element styles apply class to wrapper when it has other classes' => array( + 'color_settings' => array( 'link' => true ), + 'elements_styles' => array( + 'link' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

Hello WordPress!

', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + ), + 'element styles apply class to wrapper when it has other attributes' => array( + 'color_settings' => array( 'link' => true ), + 'elements_styles' => array( + 'link' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

Hello WordPress!

', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + ), + ); + } + + /** + * Tests that elements block support generates appropriate styles. + * + * @covers ::gutenberg_render_elements_support_styles + * + * @dataProvider data_elements_block_support_styles + * + * @param mixed $color_settings The color block support settings used for elements support. + * @param mixed $elements_styles The elements styles within the block attributes. + * @param string $expected_styles Expected styles enqueued by the style engine. + */ + public function test_elements_block_support_styles( $color_settings, $elements_styles, $expected_styles ) { + $this->register_block_with_color_settings( $color_settings ); + + $block = array( + 'blockName' => $this->test_block_name, + 'attrs' => array( + 'style' => array( + 'elements' => $elements_styles, + ), + ), + ); + + gutenberg_render_elements_support_styles( null, $block ); + $actual_stylesheet = gutenberg_style_engine_get_stylesheet_from_context( 'block-supports' ); + + $this->assertMatchesRegularExpression( + $expected_styles, + $actual_stylesheet, + 'Elements style rules output should be correct' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_elements_block_support_styles() { + $color_styles = array( + 'text' => 'var:preset|color|vivid-red', + 'background' => '#fff', + ); + $color_css_rules = preg_quote( '{color:var(--wp--preset--color--vivid-red);background-color:#fff;}' ); + + return array( + 'button element styles are not applied if serialization is skipped' => array( + 'color_settings' => array( + 'button' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'button' => array( 'color' => $color_styles ), + ), + 'expected_styles' => '/^$/', + ), + 'link element styles are not applied if serialization is skipped' => array( + 'color_settings' => array( + 'link' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'link' => array( + 'color' => $color_styles, + ':hover' => array( + 'color' => $color_styles, + ), + ), + ), + 'expected_styles' => '/^$/', + ), + 'heading element styles are not applied if serialization is skipped' => array( + 'color_settings' => array( + 'heading' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'heading' => array( 'color' => $color_styles ), + 'h1' => array( 'color' => $color_styles ), + 'h2' => array( 'color' => $color_styles ), + 'h3' => array( 'color' => $color_styles ), + 'h4' => array( 'color' => $color_styles ), + 'h5' => array( 'color' => $color_styles ), + 'h6' => array( 'color' => $color_styles ), + ), + 'expected_styles' => '/^$/', + ), + 'button element styles are applied' => array( + 'color_settings' => array( 'button' => true ), + 'elements_styles' => array( + 'button' => array( 'color' => $color_styles ), + ), + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} .wp-element-button, .wp-elements-[a-f0-9]{32} .wp-block-button__link' . $color_css_rules . '$/', + ), + 'link element styles are applied' => array( + 'color_settings' => array( 'link' => true ), + 'elements_styles' => array( + 'link' => array( + 'color' => $color_styles, + ':hover' => array( + 'color' => $color_styles, + ), + ), + ), + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} a' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} a:hover' . $color_css_rules . '$/', + ), + 'generic heading element styles are applied' => array( + 'color_settings' => array( 'heading' => true ), + 'elements_styles' => array( + 'heading' => array( 'color' => $color_styles ), + ), + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} h1, .wp-elements-[a-f0-9]{32} h2, .wp-elements-[a-f0-9]{32} h3, .wp-elements-[a-f0-9]{32} h4, .wp-elements-[a-f0-9]{32} h5, .wp-elements-[a-f0-9]{32} h6' . $color_css_rules . '$/', + ), + 'individual heading element styles are applied' => array( + 'color_settings' => array( 'heading' => true ), + 'elements_styles' => array( + 'h1' => array( 'color' => $color_styles ), + 'h2' => array( 'color' => $color_styles ), + 'h3' => array( 'color' => $color_styles ), + 'h4' => array( 'color' => $color_styles ), + 'h5' => array( 'color' => $color_styles ), + 'h6' => array( 'color' => $color_styles ), + ), + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} h1' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} h2' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} h3' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} h4' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} h5' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} h6' . $color_css_rules . '$/', + ), + ); + } +} diff --git a/phpunit/bootstrap.php b/phpunit/bootstrap.php index 7084df68443baa..acc7cfde89dbd4 100644 --- a/phpunit/bootstrap.php +++ b/phpunit/bootstrap.php @@ -94,6 +94,7 @@ function fail_if_died( $message ) { 'gutenberg-experiments' => array( 'gutenberg-widget-experiments' => '1', 'gutenberg-full-site-editing' => 1, + 'gutenberg-form-blocks' => 1, ), ); diff --git a/platform-docs/docs/basic-concepts/internationalization.md b/platform-docs/docs/basic-concepts/internationalization.md index c15a1e2e68145f..654d70070f1cba 100644 --- a/platform-docs/docs/basic-concepts/internationalization.md +++ b/platform-docs/docs/basic-concepts/internationalization.md @@ -4,4 +4,36 @@ sidebar_position: 7 # Internationalization +The Gutenberg block editor uses the `@wordpress/i18n` package to provide internationalization support. + +Translations can be provided by calling the `setLocaleData` function with a domain and a locale data object. The locale data object should be in the [Jed-formatted JSON object shape](http://messageformat.github.io/Jed/). + +```js +import { setLocaleData } from '@wordpress/i18n'; + +setLocaleData( { 'Type / to choose a block': [ 'Taper / pour choisir un bloc' ] } ); +``` + # RTL Support + +By default, the Gutenberg UI is optimized for left-to-right (LTR) languages. But Gutenberg scripts and styles include support for right-to-left (RTL) languages as well. To enable RTL support, we need to perform a few actions: + +First, we need to define that our locale is RTL using `@wordpress/i18n`. + +```js +import { setLocaleData } from '@wordpress/i18n'; + +setLocaleData( { 'text direction\u0004ltr': [ 'rtl' ] } ); +``` + +Second, we need to load the RTL CSS stylesheets instead of the LTR ones. For each of the stylesheets that you load from `@wordpress` packages, there's an RTL version that you can use instead. + +For example, when loading the `@wordpress/components` stylesheet, you can load the RTL version by using `@wordpress/components/build-style/style-rtl.css` instead of `@wordpress/components/build-style/style.css`. + +Finally, make sure to add a `dir` property to the `html` element of your document (or any parent element of your editor). + +```html + + + +``` diff --git a/test/e2e/specs/editor/blocks/buttons.spec.js b/test/e2e/specs/editor/blocks/buttons.spec.js index 04cd358729a113..dcddfca2b5b284 100644 --- a/test/e2e/specs/editor/blocks/buttons.spec.js +++ b/test/e2e/specs/editor/blocks/buttons.spec.js @@ -30,7 +30,9 @@ test.describe( 'Buttons', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/buttons' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'Content' ); diff --git a/test/e2e/specs/editor/blocks/classic.spec.js b/test/e2e/specs/editor/blocks/classic.spec.js index 30f6051fab29bc..2dcc526851743e 100644 --- a/test/e2e/specs/editor/blocks/classic.spec.js +++ b/test/e2e/specs/editor/blocks/classic.spec.js @@ -18,15 +18,10 @@ test.use( { } ); test.describe( 'Classic', () => { - test.beforeEach( async ( { admin, page } ) => { + test.beforeEach( async ( { admin, editor } ) => { await admin.createNewPost(); // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); + await editor.switchToLegacyCanvas(); } ); test.afterAll( async ( { requestUtils } ) => { @@ -134,12 +129,7 @@ test.describe( 'Classic', () => { await page.unroute( '**' ); // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); + await editor.switchToLegacyCanvas(); const errors = []; page.on( 'pageerror', ( exception ) => { diff --git a/test/e2e/specs/editor/blocks/code.spec.js b/test/e2e/specs/editor/blocks/code.spec.js index c4037d50b7dd51..6abfb15d10b83b 100644 --- a/test/e2e/specs/editor/blocks/code.spec.js +++ b/test/e2e/specs/editor/blocks/code.spec.js @@ -12,7 +12,9 @@ test.describe( 'Code', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '```' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( ' { await expect( warning ).toBeVisible(); await expect( placeholder ).toBeVisible(); - await editor.canvas.click( - 'role=button[name="Switch to editable mode"i]' - ); + await editor.canvas + .locator( 'role=button[name="Switch to editable mode"i]' ) + .click(); const commentTemplate = editor.canvas.locator( 'role=document[name="Block: Comment Template"i]' diff --git a/test/e2e/specs/editor/blocks/gallery.spec.js b/test/e2e/specs/editor/blocks/gallery.spec.js index f950036539c11b..ef693cb8b5bb9e 100644 --- a/test/e2e/specs/editor/blocks/gallery.spec.js +++ b/test/e2e/specs/editor/blocks/gallery.spec.js @@ -51,7 +51,9 @@ test.describe( 'Gallery', () => { plainText: `[gallery ids="${ uploadedMedia.id }"]`, } ); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await pageUtils.pressKeys( 'primary+v' ); const img = editor.canvas.locator( @@ -204,7 +206,9 @@ test.describe( 'Gallery', () => { } ) => { await admin.createNewPost(); await editor.insertBlock( { name: 'core/gallery' } ); - await editor.canvas.click( 'role=button[name="Media Library"i]' ); + await editor.canvas + .locator( 'role=button[name="Media Library"i]' ) + .click(); const mediaLibrary = page.locator( 'role=dialog[name="Create gallery"i]' diff --git a/test/e2e/specs/editor/blocks/group.spec.js b/test/e2e/specs/editor/blocks/group.spec.js index 2de22245eb5b08..ccf522d8c4d533 100644 --- a/test/e2e/specs/editor/blocks/group.spec.js +++ b/test/e2e/specs/editor/blocks/group.spec.js @@ -29,9 +29,11 @@ test.describe( 'Group', () => { ); // Select the default, selected Group layout from the variation picker. - await editor.canvas.click( - 'role=button[name="Group: Gather blocks in a container."i]' - ); + await editor.canvas + .locator( + 'role=button[name="Group: Gather blocks in a container."i]' + ) + .click(); expect( await editor.getEditedPostContent() ).toMatchSnapshot(); } ); @@ -40,7 +42,9 @@ test.describe( 'Group', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/group' ); await expect( page.locator( 'role=option[name="Group"i][selected]' ) @@ -48,9 +52,11 @@ test.describe( 'Group', () => { await page.keyboard.press( 'Enter' ); // Select the default, selected Group layout from the variation picker. - await editor.canvas.click( - 'role=button[name="Group: Gather blocks in a container."i]' - ); + await editor.canvas + .locator( + 'role=button[name="Group: Gather blocks in a container."i]' + ) + .click(); expect( await editor.getEditedPostContent() ).toMatchSnapshot(); } ); @@ -60,10 +66,12 @@ test.describe( 'Group', () => { page, } ) => { await editor.insertBlock( { name: 'core/group' } ); - await editor.canvas.click( - 'button[aria-label="Group: Gather blocks in a container."]' - ); - await editor.canvas.click( 'role=button[name="Add block"i]' ); + await editor.canvas + .locator( + 'button[aria-label="Group: Gather blocks in a container."]' + ) + .click(); + await editor.canvas.locator( 'role=button[name="Add block"i]' ).click(); await page.click( 'role=listbox[name="Blocks"i] >> role=option[name="Paragraph"i]' ); diff --git a/test/e2e/specs/editor/blocks/heading.spec.js b/test/e2e/specs/editor/blocks/heading.spec.js index 4af974d7b4af6f..705bce2c3f2c9a 100644 --- a/test/e2e/specs/editor/blocks/heading.spec.js +++ b/test/e2e/specs/editor/blocks/heading.spec.js @@ -12,7 +12,9 @@ test.describe( 'Heading', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '### 3' ); await expect.poll( editor.getBlocks ).toMatchObject( [ @@ -27,7 +29,9 @@ test.describe( 'Heading', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '4' ); await page.keyboard.press( 'ArrowLeft' ); await page.keyboard.type( '#### ' ); @@ -44,7 +48,9 @@ test.describe( 'Heading', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '## 1. H' ); await expect.poll( editor.getBlocks ).toMatchObject( [ @@ -59,7 +65,9 @@ test.describe( 'Heading', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '## `code`' ); await expect.poll( editor.getBlocks ).toMatchObject( [ @@ -115,7 +123,9 @@ test.describe( 'Heading', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '### Heading' ); await editor.openDocumentSettingsSidebar(); @@ -147,7 +157,9 @@ test.describe( 'Heading', () => { } ); test( 'should correctly apply named colors', async ( { editor, page } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '## Heading' ); await editor.openDocumentSettingsSidebar(); @@ -185,7 +197,9 @@ test.describe( 'Heading', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '## Heading' ); // Change text alignment @@ -216,7 +230,9 @@ test.describe( 'Heading', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Paragraph' ); // Change text alignment @@ -247,7 +263,9 @@ test.describe( 'Heading', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '## Heading' ); // Change text alignment diff --git a/test/e2e/specs/editor/blocks/html.spec.js b/test/e2e/specs/editor/blocks/html.spec.js index 99a875f0810183..f034da6efe6173 100644 --- a/test/e2e/specs/editor/blocks/html.spec.js +++ b/test/e2e/specs/editor/blocks/html.spec.js @@ -10,7 +10,9 @@ test.describe( 'HTML block', () => { test( 'can be created by typing "/html"', async ( { editor, page } ) => { // Create a Custom HTML block with the slash shortcut. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); await page.keyboard.type( '/html' ); await expect( page.locator( 'role=option[name="Custom HTML"i][selected]' ) @@ -33,7 +35,9 @@ test.describe( 'HTML block', () => { test( 'should not encode <', async ( { editor, page } ) => { // Create a Custom HTML block with the slash shortcut. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); await page.keyboard.type( '/html' ); await expect( page.locator( 'role=option[name="Custom HTML"i][selected]' ) @@ -42,7 +46,6 @@ test.describe( 'HTML block', () => { await page.keyboard.type( '1 < 2' ); await editor.publishPost(); await page.reload(); - await page.waitForSelector( '[name="editor-canvas"]' ); await expect( editor.canvas.locator( '[data-type="core/html"] textarea' ) ).toBeVisible(); diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index b538e7598d96bc..db3ff72e3ab6eb 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -82,9 +82,9 @@ test.describe( 'Image', () => { await page.keyboard.type( '2' ); expect( - await editor.canvas.evaluate( - () => document.activeElement.innerHTML - ) + await editor.canvas + .locator( ':root' ) + .evaluate( () => document.activeElement.innerHTML ) ).toBe( '12' ); } ); @@ -112,9 +112,9 @@ test.describe( 'Image', () => { await page.keyboard.press( 'Enter' ); expect( - await editor.canvas.evaluate( - () => document.activeElement.innerHTML - ) + await editor.canvas + .locator( ':root' ) + .evaluate( () => document.activeElement.innerHTML ) ).toBe( '1
2' ); } ); @@ -166,9 +166,9 @@ test.describe( 'Image', () => { await page.keyboard.press( 'ArrowRight' ); expect( - await editor.canvas.evaluate( - () => document.activeElement.innerHTML - ) + await editor.canvas + .locator( ':root' ) + .evaluate( () => document.activeElement.innerHTML ) ).toBe( 'a' ); } ); @@ -392,7 +392,7 @@ test.describe( 'Image', () => { ); await expect( image ).toHaveAttribute( 'src', new RegExp( filename ) ); - await editor.canvas.focus( '.wp-block-image' ); + await editor.canvas.locator( '.wp-block-image' ).focus(); await pageUtils.pressKeys( 'primary+z' ); // Expect an empty image block (placeholder) rather than one with a @@ -406,13 +406,6 @@ test.describe( 'Image', () => { page, editor, } ) => { - // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); await editor.insertBlock( { name: 'core/image' } ); const imageBlock = editor.canvas.getByRole( 'document', { name: 'Block: Image', diff --git a/test/e2e/specs/editor/blocks/list.spec.js b/test/e2e/specs/editor/blocks/list.spec.js index f4396982bb997f..6716a8fb5eac41 100644 --- a/test/e2e/specs/editor/blocks/list.spec.js +++ b/test/e2e/specs/editor/blocks/list.spec.js @@ -13,7 +13,9 @@ test.describe( 'List (@firefox)', () => { page, } ) => { // Create a block with some text that will trigger a list creation. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* A list item' ); // Create a second list item. @@ -38,7 +40,9 @@ test.describe( 'List (@firefox)', () => { pageUtils, } ) => { // Create a list with the slash block shortcut. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'test' ); await pageUtils.pressKeys( 'ArrowLeft', { times: 4 } ); await page.keyboard.type( '* ' ); @@ -56,7 +60,9 @@ test.describe( 'List (@firefox)', () => { page, } ) => { // Create a block with some text that will trigger a list creation. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1) A list item' ); await expect.poll( editor.getEditedPostContent ).toBe( @@ -73,7 +79,9 @@ test.describe( 'List (@firefox)', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1. ' ); await pageUtils.pressKeys( 'primary+z' ); @@ -88,7 +96,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* ' ); await page.keyboard.press( 'Backspace' ); @@ -103,7 +113,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* ' ); await expect( editor.canvas.locator( '[data-type="core/list"]' ) @@ -121,7 +133,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* ' ); await editor.showBlockToolbar(); await page.keyboard.press( 'Backspace' ); @@ -137,7 +151,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.evaluate( () => delete window.requestIdleCallback ); await page.keyboard.type( '* ' ); await expect( @@ -156,7 +172,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* ' ); await page.keyboard.press( 'Escape' ); @@ -171,7 +189,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* a' ); await page.keyboard.press( 'Backspace' ); await page.keyboard.press( 'Backspace' ); @@ -183,7 +203,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* ' ); await expect( editor.canvas.locator( '[data-type="core/list"]' ) @@ -200,7 +222,9 @@ test.describe( 'List (@firefox)', () => { test( 'can be created by typing "/list"', async ( { editor, page } ) => { // Create a list with the slash block shortcut. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/list' ); await expect( page.locator( 'role=option[name="List"i][selected]' ) @@ -221,7 +245,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'test' ); await editor.transformBlockTo( 'core/list' ); @@ -238,12 +264,16 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'one' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'two' ); await page.keyboard.down( 'Shift' ); - await editor.canvas.click( '[data-type="core/paragraph"] >> nth=0' ); + await editor.canvas + .locator( '[data-type="core/paragraph"] >> nth=0' ) + .click(); await page.keyboard.up( 'Shift' ); await editor.transformBlockTo( 'core/list' ); @@ -265,7 +295,9 @@ test.describe( 'List (@firefox)', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'one' ); await pageUtils.pressKeys( 'shift+Enter' ); await page.keyboard.type( 'two' ); @@ -289,14 +321,18 @@ test.describe( 'List (@firefox)', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'one' ); await pageUtils.pressKeys( 'shift+Enter' ); await page.keyboard.type( '...' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'two' ); await page.keyboard.down( 'Shift' ); - await editor.canvas.click( '[data-type="core/paragraph"] >> nth=0' ); + await editor.canvas + .locator( '[data-type="core/paragraph"] >> nth=0' ) + .click(); await page.keyboard.up( 'Shift' ); await editor.transformBlockTo( 'core/list' ); @@ -559,7 +595,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1. one' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'two' ); @@ -901,7 +939,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* 1' ); // Should be at level 0. await page.keyboard.press( 'Enter' ); @@ -1015,7 +1055,9 @@ test.describe( 'List (@firefox)', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* 1' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( ' a' ); @@ -1046,7 +1088,9 @@ test.describe( 'List (@firefox)', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* 1' ); await page.keyboard.press( 'Enter' ); @@ -1069,7 +1113,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); // Tests the shortcut with a non breaking space. await page.keyboard.type( '*\u00a0' ); @@ -1085,7 +1131,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); // Tests the shortcut with a non breaking space. await page.keyboard.type( '* 1' ); @@ -1149,7 +1197,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* 1' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '2' ); @@ -1174,7 +1224,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* 1' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '2' ); @@ -1204,7 +1256,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1. a' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'b' ); @@ -1261,7 +1315,9 @@ test.describe( 'List (@firefox)', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* a' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'b' ); @@ -1304,7 +1360,9 @@ test.describe( 'List (@firefox)', () => { } ); test( 'can be exited to selected paragraph', async ( { editor, page } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* ' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '1' ); diff --git a/test/e2e/specs/editor/blocks/navigation-colors.spec.js b/test/e2e/specs/editor/blocks/navigation-colors.spec.js index 42ecb29aa6650f..1ddd4af8ab2e13 100644 --- a/test/e2e/specs/editor/blocks/navigation-colors.spec.js +++ b/test/e2e/specs/editor/blocks/navigation-colors.spec.js @@ -383,12 +383,12 @@ class ColorControl { await customLink.click(); // Submenu elements. - const submenuLink = this.editor.canvas - .locator( 'a' ) - .filter( { hasText: 'Submenu Link' } ); const submenuWrapper = this.editor.canvas .getByRole( 'document', { name: 'Block: Custom Link' } ) - .filter( { has: submenuLink } ); + .filter( { hasText: 'Submenu Link' } ); + const submenuLink = submenuWrapper + .locator( 'a' ) + .filter( { hasText: 'Submenu Link' } ); // Submenu link color. await expect( submenuLink ).toHaveCSS( 'color', submenuTextColor ); diff --git a/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js b/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js index 1c745d7a0e57ba..7e761f1861856f 100644 --- a/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js +++ b/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js @@ -27,7 +27,7 @@ test.describe( 'Navigation block - Frontend interactivity', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await requestUtils.createNavigationMenu( { title: 'Hidden menu', content: ` @@ -88,7 +88,7 @@ test.describe( 'Navigation block - Frontend interactivity', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await requestUtils.createNavigationMenu( { title: 'Hidden menu', content: ` @@ -203,7 +203,7 @@ test.describe( 'Navigation block - Frontend interactivity', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await requestUtils.createNavigationMenu( { title: 'Hidden menu', content: ` @@ -268,7 +268,7 @@ test.describe( 'Navigation block - Frontend interactivity', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await requestUtils.createNavigationMenu( { title: 'Page list menu', content: ` diff --git a/test/e2e/specs/editor/blocks/paragraph.spec.js b/test/e2e/specs/editor/blocks/paragraph.spec.js index fcc15890a82492..3cf3654870a351 100644 --- a/test/e2e/specs/editor/blocks/paragraph.spec.js +++ b/test/e2e/specs/editor/blocks/paragraph.spec.js @@ -28,9 +28,11 @@ test.describe( 'Paragraph', () => { } ); await page.keyboard.type( '1' ); - const firstBlockTagName = await editor.canvas.evaluate( () => { - return document.querySelector( '[data-block]' ).tagName; - } ); + const firstBlockTagName = await editor.canvas + .locator( ':root' ) + .evaluate( () => { + return document.querySelector( '[data-block]' ).tagName; + } ); // The outer element should be a paragraph. Blocks should never have any // additional div wrappers so the markup remains simple and easy to @@ -61,14 +63,7 @@ test.describe( 'Paragraph', () => { editor, pageUtils, draggingUtils, - page, } ) => { - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); await editor.insertBlock( { name: 'core/paragraph' } ); const testImageName = '10x10_e2e_test_image_z9T8jK.png'; @@ -112,7 +107,7 @@ test.describe( 'Paragraph', () => { attributes: { content: 'My Heading' }, } ); await editor.insertBlock( { name: 'core/paragraph' } ); - await editor.canvas.focus( 'text=My Heading' ); + await editor.canvas.locator( 'text=My Heading' ).focus(); await editor.showBlockToolbar(); const dragHandle = page.locator( diff --git a/test/e2e/specs/editor/blocks/pullquote.spec.js b/test/e2e/specs/editor/blocks/pullquote.spec.js index f2a6698f5065ff..33f833ca536788 100644 --- a/test/e2e/specs/editor/blocks/pullquote.spec.js +++ b/test/e2e/specs/editor/blocks/pullquote.spec.js @@ -12,7 +12,9 @@ test.describe( 'Quote', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'test' ); await editor.transformBlockTo( 'core/quote' ); diff --git a/test/e2e/specs/editor/blocks/quote.spec.js b/test/e2e/specs/editor/blocks/quote.spec.js index 13b7ee341ede72..d25dedd4a0a397 100644 --- a/test/e2e/specs/editor/blocks/quote.spec.js +++ b/test/e2e/specs/editor/blocks/quote.spec.js @@ -33,7 +33,9 @@ test.describe( 'Quote', () => { page, } ) => { // Create a block with some text that will trigger a paragraph creation. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '> A quote' ); // Create a second paragraph. await page.keyboard.press( 'Enter' ); @@ -56,7 +58,9 @@ test.describe( 'Quote', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'test' ); await pageUtils.pressKeys( 'ArrowLeft', { times: 'test'.length } ); await page.keyboard.type( '> ' ); @@ -71,7 +75,9 @@ test.describe( 'Quote', () => { test( 'can be created by typing "/quote"', async ( { editor, page } ) => { // Create a list with the slash block shortcut. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/quote' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'I’m a quote' ); @@ -88,7 +94,9 @@ test.describe( 'Quote', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'test' ); await editor.transformBlockTo( 'core/quote' ); expect( await editor.getEditedPostContent() ).toBe( @@ -104,14 +112,16 @@ test.describe( 'Quote', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'one' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'two' ); await page.keyboard.down( 'Shift' ); - await editor.canvas.click( - 'role=document[name="Block: Paragraph"i] >> text=one' - ); + await editor.canvas + .locator( 'role=document[name="Block: Paragraph"i] >> text=one' ) + .click(); await page.keyboard.up( 'Shift' ); await editor.transformBlockTo( 'core/quote' ); expect( await editor.getEditedPostContent() ).toBe( diff --git a/test/e2e/specs/editor/blocks/separator.spec.js b/test/e2e/specs/editor/blocks/separator.spec.js index a2e088e14c3983..70c61535e71bf7 100644 --- a/test/e2e/specs/editor/blocks/separator.spec.js +++ b/test/e2e/specs/editor/blocks/separator.spec.js @@ -12,7 +12,9 @@ test.describe( 'Separator', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '---' ); await page.keyboard.press( 'Enter' ); diff --git a/test/e2e/specs/editor/blocks/spacer.spec.js b/test/e2e/specs/editor/blocks/spacer.spec.js index 77e978a0df3027..f089402514623c 100644 --- a/test/e2e/specs/editor/blocks/spacer.spec.js +++ b/test/e2e/specs/editor/blocks/spacer.spec.js @@ -10,7 +10,9 @@ test.describe( 'Spacer', () => { test( 'can be created by typing "/spacer"', async ( { editor, page } ) => { // Create a spacer with the slash block shortcut. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/spacer' ); await page.keyboard.press( 'Enter' ); @@ -22,7 +24,9 @@ test.describe( 'Spacer', () => { editor, } ) => { // Create a spacer with the slash block shortcut. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/spacer' ); await page.keyboard.press( 'Enter' ); diff --git a/test/e2e/specs/editor/blocks/table.spec.js b/test/e2e/specs/editor/blocks/table.spec.js index 689989f9022a3d..1e6dfdcd76e188 100644 --- a/test/e2e/specs/editor/blocks/table.spec.js +++ b/test/e2e/specs/editor/blocks/table.spec.js @@ -39,7 +39,9 @@ test.describe( 'Table', () => { await page.keyboard.type( '10' ); // Create the table. - await editor.canvas.click( 'role=button[name="Create Table"i]' ); + await editor.canvas + .locator( 'role=button[name="Create Table"i]' ) + .click(); // Expect the post content to have a correctly sized table. expect( await editor.getEditedPostContent() ).toMatchSnapshot(); @@ -49,12 +51,14 @@ test.describe( 'Table', () => { await editor.insertBlock( { name: 'core/table' } ); // Create the table. - await editor.canvas.click( 'role=button[name="Create Table"i]' ); + await editor.canvas + .locator( 'role=button[name="Create Table"i]' ) + .click(); // Click the first cell and add some text. - await editor.canvas.click( - 'role=textbox[name="Body cell text"i] >> nth=0' - ); + await editor.canvas + .locator( 'role=textbox[name="Body cell text"i] >> nth=0' ) + .click(); await page.keyboard.type( 'This' ); // Navigate to the next cell and add some text. @@ -92,7 +96,9 @@ test.describe( 'Table', () => { await expect( footerSwitch ).toBeHidden(); // // Create the table. - await editor.canvas.click( 'role=button[name="Create Table"i]' ); + await editor.canvas + .locator( 'role=button[name="Create Table"i]' ) + .click(); // Expect the header and footer switches to be present now that the table has been created. await page.click( @@ -105,19 +111,25 @@ test.describe( 'Table', () => { await headerSwitch.check(); await footerSwitch.check(); - await editor.canvas.click( - 'role=rowgroup >> nth=0 >> role=textbox[name="Header cell text"i] >> nth=0' - ); + await editor.canvas + .locator( + 'role=rowgroup >> nth=0 >> role=textbox[name="Header cell text"i] >> nth=0' + ) + .click(); await page.keyboard.type( 'header' ); - await editor.canvas.click( - 'role=rowgroup >> nth=1 >> role=textbox[name="Body cell text"i] >> nth=0' - ); + await editor.canvas + .locator( + 'role=rowgroup >> nth=1 >> role=textbox[name="Body cell text"i] >> nth=0' + ) + .click(); await page.keyboard.type( 'body' ); - await editor.canvas.click( - 'role=rowgroup >> nth=2 >> role=textbox[name="Footer cell text"i] >> nth=0' - ); + await editor.canvas + .locator( + 'role=rowgroup >> nth=2 >> role=textbox[name="Footer cell text"i] >> nth=0' + ) + .click(); await page.keyboard.type( 'footer' ); // Expect the table to have a header, body and footer with written content. @@ -139,7 +151,9 @@ test.describe( 'Table', () => { await editor.openDocumentSettingsSidebar(); // Create the table. - await editor.canvas.click( 'role=button[name="Create Table"i]' ); + await editor.canvas + .locator( 'role=button[name="Create Table"i]' ) + .click(); // Toggle on the switches and add some content. await page.click( @@ -147,9 +161,9 @@ test.describe( 'Table', () => { ); await page.locator( 'role=checkbox[name="Header section"i]' ).check(); await page.locator( 'role=checkbox[name="Footer section"i]' ).check(); - await editor.canvas.click( - 'role=textbox[name="Body cell text"i] >> nth=0' - ); + await editor.canvas + .locator( 'role=textbox[name="Body cell text"i] >> nth=0' ) + .click(); // Add a column. await editor.clickBlockToolbarButton( 'Edit table' ); @@ -158,9 +172,9 @@ test.describe( 'Table', () => { // Expect the table to have 3 columns across the header, body and footer. expect( await editor.getEditedPostContent() ).toMatchSnapshot(); - await editor.canvas.click( - 'role=textbox[name="Body cell text"i] >> nth=0' - ); + await editor.canvas + .locator( 'role=textbox[name="Body cell text"i] >> nth=0' ) + .click(); // Delete a column. await editor.clickBlockToolbarButton( 'Edit table' ); @@ -173,12 +187,16 @@ test.describe( 'Table', () => { test( 'allows columns to be aligned', async ( { editor, page } ) => { await editor.insertBlock( { name: 'core/table' } ); - await editor.canvas.click( 'role=spinbutton[name="Column count"i]' ); + await editor.canvas + .locator( 'role=spinbutton[name="Column count"i]' ) + .click(); await page.keyboard.press( 'Backspace' ); await page.keyboard.type( '4' ); // Create the table. - await editor.canvas.click( 'role=button[name="Create Table"i]' ); + await editor.canvas + .locator( 'role=button[name="Create Table"i]' ) + .click(); // Click the first cell and add some text. Don't align. const cells = editor.canvas.locator( @@ -218,7 +236,9 @@ test.describe( 'Table', () => { await editor.openDocumentSettingsSidebar(); // Create the table. - await editor.canvas.click( 'role=button[name="Create Table"i]' ); + await editor.canvas + .locator( 'role=button[name="Create Table"i]' ) + .click(); // Enable fixed width as it exacerbates the amount of empty space around the RichText. await page.click( @@ -229,9 +249,9 @@ test.describe( 'Table', () => { .check(); // Add multiple new lines to the first cell to make it taller. - await editor.canvas.click( - 'role=textbox[name="Body cell text"i] >> nth=0' - ); + await editor.canvas + .locator( 'role=textbox[name="Body cell text"i] >> nth=0' ) + .click(); await page.keyboard.type( '\n\n\n\n' ); // Get the bounding client rect for the second cell. @@ -251,12 +271,14 @@ test.describe( 'Table', () => { await editor.insertBlock( { name: 'core/table' } ); // Create the table. - await editor.canvas.click( 'role=button[name="Create Table"i]' ); + await editor.canvas + .locator( 'role=button[name="Create Table"i]' ) + .click(); // Click the first cell and add some text. - await editor.canvas.click( - 'role=document[name="Block: Table"i] >> figcaption' - ); + await editor.canvas + .locator( 'role=document[name="Block: Table"i] >> figcaption' ) + .click(); await page.keyboard.type( 'Caption!' ); expect( await editor.getEditedPostContent() ).toMatchSnapshot(); } ); @@ -264,7 +286,9 @@ test.describe( 'Table', () => { test( 'up and down arrow navigation', async ( { editor, page } ) => { await editor.insertBlock( { name: 'core/table' } ); // Create the table. - await editor.canvas.click( 'role=button[name="Create Table"i]' ); + await editor.canvas + .locator( 'role=button[name="Create Table"i]' ) + .click(); await page.keyboard.type( '1' ); await page.keyboard.press( 'ArrowDown' ); await page.keyboard.type( '2' ); @@ -280,7 +304,9 @@ test.describe( 'Table', () => { await editor.insertBlock( { name: 'core/table' } ); // Create the table. - await editor.canvas.click( 'role=button[name="Create Table"i]' ); + await editor.canvas + .locator( 'role=button[name="Create Table"i]' ) + .click(); // Focus should be in first td. await expect( diff --git a/test/e2e/specs/editor/local/demo.spec.js b/test/e2e/specs/editor/local/demo.spec.js new file mode 100644 index 00000000000000..acfee9296e2324 --- /dev/null +++ b/test/e2e/specs/editor/local/demo.spec.js @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'New editor state', () => { + test( 'content should load, making the post dirty', async ( { + page, + admin, + } ) => { + await admin.visitAdminPage( 'post-new.php', 'gutenberg-demo' ); + await page.waitForFunction( () => { + if ( ! window?.wp?.data?.dispatch ) { + return false; + } + window.wp.data + .dispatch( 'core/preferences' ) + .set( 'core/edit-post', 'welcomeGuide', false ); + + window.wp.data + .dispatch( 'core/preferences' ) + .set( 'core/edit-post', 'fullscreenMode', false ); + + return true; + } ); + + const isDirty = await page.evaluate( () => { + return window.wp.data.select( 'core/editor' ).isEditedPostDirty(); + } ); + expect( isDirty ).toBe( true ); + + await expect( + page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Save draft' } ) + ).toBeEnabled(); + } ); +} ); diff --git a/test/e2e/specs/editor/plugins/block-variations.spec.js b/test/e2e/specs/editor/plugins/block-variations.spec.js index 0b445aee451c66..9f12c987efd39b 100644 --- a/test/e2e/specs/editor/plugins/block-variations.spec.js +++ b/test/e2e/specs/editor/plugins/block-variations.spec.js @@ -48,7 +48,9 @@ test.describe( 'Block variations', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/Large Quote' ); await page.keyboard.press( 'Enter' ); @@ -82,7 +84,9 @@ test.describe( 'Block variations', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/Heading' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '/Success Message' ); @@ -97,7 +101,9 @@ test.describe( 'Block variations', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/Columns' ); await page.keyboard.press( 'Enter' ); @@ -120,7 +126,9 @@ test.describe( 'Block variations', () => { pageUtils, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/Large Quote' ); await page.keyboard.press( 'Enter' ); @@ -155,7 +163,9 @@ test.describe( 'Block variations', () => { pageUtils, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/Heading' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '/Success Message' ); @@ -189,7 +199,9 @@ test.describe( 'Block variations', () => { pageUtils, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/Heading' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '/Warning Message' ); diff --git a/test/e2e/specs/editor/plugins/custom-post-types.spec.js b/test/e2e/specs/editor/plugins/custom-post-types.spec.js index 1cceddbf04ad12..17a497f26cee02 100644 --- a/test/e2e/specs/editor/plugins/custom-post-types.spec.js +++ b/test/e2e/specs/editor/plugins/custom-post-types.spec.js @@ -20,7 +20,9 @@ test.describe( 'Test Custom Post Types', () => { page, } ) => { await admin.createNewPost( { postType: 'hierar-no-title' } ); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Parent Post' ); await editor.publishPost(); @@ -53,7 +55,9 @@ test.describe( 'Test Custom Post Types', () => { await page.getByRole( 'listbox' ).getByRole( 'option' ).first().click(); const parentPage = await parentPageLocator.inputValue(); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Child Post' ); await editor.publishPost(); await page.reload(); @@ -68,7 +72,9 @@ test.describe( 'Test Custom Post Types', () => { page, } ) => { await admin.createNewPost( { postType: 'leg_block_in_tpl' } ); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Hello there' ); await expect.poll( editor.getBlocks ).toMatchObject( [ diff --git a/test/e2e/specs/editor/plugins/format-api.spec.js b/test/e2e/specs/editor/plugins/format-api.spec.js index f98d8292ea8f6f..1e631615313bd1 100644 --- a/test/e2e/specs/editor/plugins/format-api.spec.js +++ b/test/e2e/specs/editor/plugins/format-api.spec.js @@ -21,7 +21,9 @@ test.describe( 'Using Format API', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'First paragraph' ); await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); await editor.clickBlockToolbarButton( 'More' ); diff --git a/test/e2e/specs/editor/plugins/hooks-api.spec.js b/test/e2e/specs/editor/plugins/hooks-api.spec.js index b3b5ed68f66f11..95bc5bf8bfd2c0 100644 --- a/test/e2e/specs/editor/plugins/hooks-api.spec.js +++ b/test/e2e/specs/editor/plugins/hooks-api.spec.js @@ -22,7 +22,9 @@ test.describe( 'Using Hooks API', () => { editor, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'First paragraph' ); await page.click( `role=region[name="Editor settings"i] >> role=tab[name="Settings"i]` @@ -37,7 +39,9 @@ test.describe( 'Using Hooks API', () => { page, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'First paragraph' ); const paragraphBlock = editor.canvas.locator( diff --git a/test/e2e/specs/editor/plugins/inner-blocks-allowed-blocks.spec.js b/test/e2e/specs/editor/plugins/inner-blocks-allowed-blocks.spec.js index cd6f440d2db0ed..c0627121f16497 100644 --- a/test/e2e/specs/editor/plugins/inner-blocks-allowed-blocks.spec.js +++ b/test/e2e/specs/editor/plugins/inner-blocks-allowed-blocks.spec.js @@ -107,7 +107,9 @@ test.describe( 'Allowed Blocks Setting on InnerBlocks', () => { editor, page, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/Allowed Blocks Dynamic' ); await page.keyboard.press( 'Enter' ); diff --git a/test/e2e/specs/editor/plugins/nonce.spec.js b/test/e2e/specs/editor/plugins/nonce.spec.js index a05f3618b36417..0ffe0c5efb8139 100644 --- a/test/e2e/specs/editor/plugins/nonce.spec.js +++ b/test/e2e/specs/editor/plugins/nonce.spec.js @@ -12,12 +12,13 @@ test.describe( 'Nonce', () => { page, admin, requestUtils, + editor, } ) => { await admin.createNewPost(); + await expect( + editor.canvas.getByRole( 'textbox', { name: 'Add title' } ) + ).toBeFocused(); await page.keyboard.press( 'Enter' ); - // Wait until the network is idle. - // eslint-disable-next-line playwright/no-networkidle - await page.waitForLoadState( 'networkidle' ); await page.keyboard.type( 'test' ); /** diff --git a/test/e2e/specs/editor/plugins/post-type-templates.spec.js b/test/e2e/specs/editor/plugins/post-type-templates.spec.js index c743d08d8ae681..9b2abddb0dd0e2 100644 --- a/test/e2e/specs/editor/plugins/post-type-templates.spec.js +++ b/test/e2e/specs/editor/plugins/post-type-templates.spec.js @@ -35,7 +35,9 @@ test.describe( 'Post type templates', () => { // Remove a block from the template to verify that it's not // re-added after saving and reloading the editor. - await editor.canvas.focus( 'role=textbox[name="Add title"i]' ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .focus(); await page.keyboard.press( 'ArrowDown' ); await page.keyboard.press( 'Backspace' ); await page.click( 'role=button[name="Save draft"i]' ); @@ -64,10 +66,9 @@ test.describe( 'Post type templates', () => { } ) => { // Remove all blocks from the template to verify that they're not // re-added after saving and reloading the editor. - await editor.canvas.fill( - 'role=textbox[name="Add title"i]', - 'My Empty Book' - ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .fill( 'My Empty Book' ); await page.keyboard.press( 'ArrowDown' ); await pageUtils.pressKeys( 'primary+A' ); await page.keyboard.press( 'Backspace' ); @@ -125,11 +126,12 @@ test.describe( 'Post type templates', () => { // Remove the default block template to verify that it's not // re-added after saving and reloading the editor. - await editor.canvas.fill( - 'role=textbox[name="Add title"i]', - 'My Image Format' - ); - await editor.canvas.focus( 'role=document[name="Block: Image"i]' ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .fill( 'My Image Format' ); + await editor.canvas + .locator( 'role=document[name="Block: Image"i]' ) + .focus(); await page.keyboard.press( 'Backspace' ); await page.click( 'role=button[name="Save draft"i]' ); await expect( diff --git a/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js b/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js index c5eafdafe918db..710e06b35e124f 100644 --- a/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js +++ b/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js @@ -20,7 +20,7 @@ test.describe( 'WP Editor Meta Boxes', () => { await admin.createNewPost(); // Add title to enable valid non-empty post save. - await editor.canvas + await page .locator( 'role=textbox[name="Add title"i]' ) .type( 'Hello Meta' ); diff --git a/test/e2e/specs/editor/various/a11y-region-navigation.spec.js b/test/e2e/specs/editor/various/a11y-region-navigation.spec.js index 2c28ba311d8c71..d8dda0cabdf843 100644 --- a/test/e2e/specs/editor/various/a11y-region-navigation.spec.js +++ b/test/e2e/specs/editor/various/a11y-region-navigation.spec.js @@ -22,6 +22,14 @@ test.describe( 'Region navigation (@firefox, @webkit)', () => { attributes: { content: 'Dummy text' }, } ); + const dummyParagraph = editor.canvas + .getByRole( 'document', { + name: 'Block: Paragraph', + } ) + .filter( { hasText: 'Dummy text' } ); + + await expect( dummyParagraph ).toBeFocused(); + // Navigate to first region and check that we made it. Must navigate forward 4 times as initial focus is placed in post title field. await page.keyboard.press( 'Control+`' ); await page.keyboard.press( 'Control+`' ); diff --git a/test/e2e/specs/editor/various/a11y.spec.js b/test/e2e/specs/editor/various/a11y.spec.js index 8b819d3866b6ca..0a5e421debedb7 100644 --- a/test/e2e/specs/editor/various/a11y.spec.js +++ b/test/e2e/specs/editor/various/a11y.spec.js @@ -23,15 +23,11 @@ test.describe( 'a11y (@firefox, @webkit)', () => { editor, } ) => { // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); + await editor.switchToLegacyCanvas(); + // On a new post, initial focus is set on the Post title. await expect( - editor.canvas.locator( 'role=textbox[name=/Add title/i]' ) + page.locator( 'role=textbox[name=/Add title/i]' ) ).toBeFocused(); // Navigate to the 'Editor settings' region. await pageUtils.pressKeys( 'ctrl+`' ); @@ -54,14 +50,11 @@ test.describe( 'a11y (@firefox, @webkit)', () => { test( 'should constrain tabbing within a modal', async ( { page, pageUtils, + editor, } ) => { // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); + await editor.switchToLegacyCanvas(); + // Open keyboard shortcuts modal. await pageUtils.pressKeys( 'access+h' ); diff --git a/test/e2e/specs/editor/various/adding-inline-tokens.spec.js b/test/e2e/specs/editor/various/adding-inline-tokens.spec.js index 0facffd9097e87..15f9d9ea877320 100644 --- a/test/e2e/specs/editor/various/adding-inline-tokens.spec.js +++ b/test/e2e/specs/editor/various/adding-inline-tokens.spec.js @@ -22,7 +22,9 @@ test.describe( 'adding inline tokens', () => { pageUtils, } ) => { // Create a paragraph. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'a ' ); diff --git a/test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js b/test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js index 3cfc838fcadf25..ec0ca999993c2f 100644 --- a/test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js +++ b/test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js @@ -100,21 +100,25 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { `; } - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( testData.triggerString ); await expect( page.locator( `role=option[name="${ testData.optionText }"i]` ) ).toBeVisible(); - const ariaOwns = await editor.canvas.evaluate( () => { - return document.activeElement.getAttribute( 'aria-owns' ); - } ); - const ariaActiveDescendant = await editor.canvas.evaluate( () => { - return document.activeElement.getAttribute( - 'aria-activedescendant' - ); - } ); + const ariaOwns = await editor.canvas + .locator( ':root' ) + .evaluate( () => { + return document.activeElement.getAttribute( 'aria-owns' ); + } ); + const ariaActiveDescendant = await editor.canvas + .locator( ':root' ) + .evaluate( () => { + return document.activeElement.getAttribute( + 'aria-activedescendant' + ); + } ); // Ensure `aria-owns` is part of the same document and ensure the // selected option is equal to the active descendant. await expect( @@ -148,9 +152,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { `; } - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Stuck in the middle with you.' ); await pageUtils.pressKeys( 'ArrowLeft', { times: 'you.'.length } ); await page.keyboard.type( testData.triggerString ); @@ -188,9 +192,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { `; } - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( testData.firstTriggerString ); await expect( page.locator( @@ -230,9 +234,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { `; } - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( testData.triggerString ); await expect( page.locator( `role=option[name="${ testData.optionText }"i]` ) @@ -269,9 +273,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { `; } - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( testData.triggerString ); await expect( page.locator( `role=option[name="${ testData.optionText }"i]` ) @@ -306,9 +310,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { `; } - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( testData.triggerString ); await expect( page.locator( `role=option[name="${ testData.optionText }"i]` ) @@ -327,9 +331,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { page, editor, } ) => { - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); // The 'Grapes' option is disabled in our test plugin, so it should not insert the grapes emoji await page.keyboard.type( 'Sorry, we are all out of ~g' ); await expect( @@ -395,9 +399,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { `; } - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); for ( let i = 0; i < 4; i++ ) { await page.keyboard.type( testData.triggerString ); @@ -423,28 +427,52 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { page, editor, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); - await page.keyboard.type( '@fr' ); + const typingDelay = 100; + + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); + + await page.keyboard.type( '@fr', { delay: typingDelay } ); await expect( - page.locator( 'role=option', { hasText: 'Frodo Baggins' } ) + page.getByRole( 'option', { + name: 'Frodo Baggins', + selected: true, + } ) ).toBeVisible(); + await page.keyboard.press( 'Enter' ); - await page.keyboard.type( ' +bi' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '@ringbearer' }, + }, + ] ); + + await page.keyboard.type( ' +bi', { delay: typingDelay } ); await expect( - page.locator( 'role=option', { hasText: 'Bilbo Baggins' } ) + page.getByRole( 'option', { + name: 'Bilbo Baggins', + selected: true, + } ) ).toBeVisible(); await page.keyboard.press( 'Enter' ); - await expect.poll( editor.getEditedPostContent ) - .toBe( ` -

@ringbearer +thebetterhobbit

-` ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '@ringbearer +thebetterhobbit' }, + }, + ] ); } ); test( 'should hide UI when selection changes (by keyboard)', async ( { page, editor, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '@fr' ); await expect( page.locator( 'role=option', { hasText: 'Frodo Baggins' } ) @@ -460,7 +488,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '@' ); await pageUtils.pressKeys( 'primary+b' ); await page.keyboard.type( 'f' ); @@ -470,7 +500,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { page.locator( 'role=option', { hasText: 'Frodo Baggins' } ) ).toBeVisible(); // Use the strong tag to move the selection by mouse within the mention. - await editor.canvas.click( '[data-type="core/paragraph"] strong' ); + await editor.canvas + .locator( '[data-type="core/paragraph"] strong' ) + .click(); await expect( page.locator( 'role=option', { hasText: 'Frodo Baggins' } ) ).toBeHidden(); @@ -480,7 +512,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { page, editor, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '/' ); await expect( page.locator( `role=option[name="Image"i]` ) diff --git a/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js b/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js index 4dc49bc0f7510a..f0bfe5bff203fb 100644 --- a/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js +++ b/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js @@ -44,9 +44,9 @@ test.describe( 'Navigating the block hierarchy', () => { } ) => { await editor.openDocumentSettingsSidebar(); await editor.insertBlock( { name: 'core/columns' } ); - await editor.canvas.click( - 'role=button[name="Two columns; equal split"i]' - ); + await editor.canvas + .locator( 'role=button[name="Two columns; equal split"i]' ) + .click(); // Open the block inserter. await page.keyboard.press( 'ArrowDown' ); @@ -99,9 +99,9 @@ test.describe( 'Navigating the block hierarchy', () => { } ) => { await editor.openDocumentSettingsSidebar(); await editor.insertBlock( { name: 'core/columns' } ); - await editor.canvas.click( - 'role=button[name="Two columns; equal split"i]' - ); + await editor.canvas + .locator( 'role=button[name="Two columns; equal split"i]' ) + .click(); // Open the block inserter. await page.keyboard.press( 'ArrowDown' ); @@ -155,7 +155,9 @@ test.describe( 'Navigating the block hierarchy', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'You say goodbye' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '## Hello, hello' ); @@ -191,9 +193,11 @@ test.describe( 'Navigating the block hierarchy', () => { pageUtils, } ) => { await editor.insertBlock( { name: 'core/group' } ); - await editor.canvas.click( - 'role=button[name="Group: Gather blocks in a container."i]' - ); + await editor.canvas + .locator( + 'role=button[name="Group: Gather blocks in a container."i]' + ) + .click(); // Open the block inserter. await page.keyboard.press( 'ArrowDown' ); @@ -225,7 +229,9 @@ test.describe( 'Navigating the block hierarchy', () => { ] ); // Deselect the blocks. - await editor.canvas.click( 'role=textbox[name="Add title"i]' ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .click(); // Open list view and return to the first block. await pageUtils.pressKeys( 'access+o' ); diff --git a/test/e2e/specs/editor/various/block-locking.spec.js b/test/e2e/specs/editor/various/block-locking.spec.js index 67a7f0357bc2a7..b40e7a4b7448a8 100644 --- a/test/e2e/specs/editor/various/block-locking.spec.js +++ b/test/e2e/specs/editor/various/block-locking.spec.js @@ -9,7 +9,9 @@ test.describe( 'Block Locking', () => { } ); test( 'can prevent removal', async ( { editor, page } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Some paragraph' ); await editor.clickBlockOptionsMenuItem( 'Lock' ); @@ -23,7 +25,9 @@ test.describe( 'Block Locking', () => { } ); test( 'can disable movement', async ( { editor, page } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'First paragraph' ); await page.keyboard.type( 'Enter' ); @@ -47,7 +51,9 @@ test.describe( 'Block Locking', () => { } ); test( 'can lock everything', async ( { editor, page } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Some paragraph' ); await editor.clickBlockOptionsMenuItem( 'Lock' ); @@ -62,7 +68,9 @@ test.describe( 'Block Locking', () => { } ); test( 'can unlock from toolbar', async ( { editor, page } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Some paragraph' ); await editor.clickBlockOptionsMenuItem( 'Lock' ); diff --git a/test/e2e/specs/editor/various/block-mover.spec.js b/test/e2e/specs/editor/various/block-mover.spec.js index 4ed90191f25585..5c3b04a0697743 100644 --- a/test/e2e/specs/editor/various/block-mover.spec.js +++ b/test/e2e/specs/editor/various/block-mover.spec.js @@ -23,7 +23,7 @@ test.describe( 'block mover', () => { } ); // Select a block so the block mover is rendered. - await editor.canvas.focus( 'text=First Paragraph' ); + await editor.canvas.locator( 'text=First Paragraph' ).focus(); await editor.showBlockToolbar(); const moveDownButton = page.locator( @@ -47,7 +47,7 @@ test.describe( 'block mover', () => { attributes: { content: 'First Paragraph' }, } ); // Select a block so the block mover has the possibility of being rendered. - await editor.canvas.focus( 'text=First Paragraph' ); + await editor.canvas.locator( 'text=First Paragraph' ).focus(); await editor.showBlockToolbar(); // Ensure no block mover exists when only one block exists on the page. diff --git a/test/e2e/specs/editor/various/block-moving-mode.spec.js b/test/e2e/specs/editor/various/block-moving-mode.spec.js index 59c53682854292..5b8ef6bdcd051b 100644 --- a/test/e2e/specs/editor/various/block-moving-mode.spec.js +++ b/test/e2e/specs/editor/various/block-moving-mode.spec.js @@ -47,9 +47,11 @@ test.describe( 'Block moving mode', () => { test( 'can move block in the nested block', async ( { editor, page } ) => { // Create two group blocks with some blocks. await editor.insertBlock( { name: 'core/group' } ); - await editor.canvas.click( - 'role=button[name="Group: Gather blocks in a container."i]' - ); + await editor.canvas + .locator( + 'role=button[name="Group: Gather blocks in a container."i]' + ) + .click(); await page.keyboard.press( 'ArrowDown' ); await page.keyboard.press( 'Enter' ); await page.getByRole( 'option', { name: 'Paragraph' } ).click(); @@ -58,9 +60,11 @@ test.describe( 'Block moving mode', () => { await page.keyboard.type( 'Second Paragraph' ); await editor.insertBlock( { name: 'core/group' } ); - await editor.canvas.click( - 'role=button[name="Group: Gather blocks in a container."i]' - ); + await editor.canvas + .locator( + 'role=button[name="Group: Gather blocks in a container."i]' + ) + .click(); await page.keyboard.press( 'ArrowDown' ); await page.keyboard.press( 'Enter' ); await page.getByRole( 'option', { name: 'Paragraph' } ).click(); @@ -122,9 +126,11 @@ test.describe( 'Block moving mode', () => { attributes: { content: 'First Paragraph' }, } ); await editor.insertBlock( { name: 'core/group' } ); - await editor.canvas.click( - 'role=button[name="Group: Gather blocks in a container."i]' - ); + await editor.canvas + .locator( + 'role=button[name="Group: Gather blocks in a container."i]' + ) + .click(); await page.keyboard.press( 'ArrowDown' ); await page.keyboard.press( 'Enter' ); await page.getByRole( 'option', { name: 'Paragraph' } ).click(); diff --git a/test/e2e/specs/editor/various/compatibility-classic-editor.spec.js b/test/e2e/specs/editor/various/compatibility-classic-editor.spec.js index a1efbfa579b9a7..71522c1d439a54 100644 --- a/test/e2e/specs/editor/various/compatibility-classic-editor.spec.js +++ b/test/e2e/specs/editor/various/compatibility-classic-editor.spec.js @@ -17,7 +17,7 @@ test.describe( 'Compatibility with classic editor', () => { editor, } ) => { await editor.insertBlock( { name: 'core/html' } ); - await editor.canvas.focus( 'role=textbox[name="HTML"i]' ); + await editor.canvas.locator( 'role=textbox[name="HTML"i]' ).focus(); await page.keyboard.type( '' ); await page.keyboard.type( 'Random Link' ); await page.keyboard.type( ' ' ); diff --git a/test/e2e/specs/editor/various/content-only-lock.spec.js b/test/e2e/specs/editor/various/content-only-lock.spec.js index 03282357a72b65..e7d52562636f3b 100644 --- a/test/e2e/specs/editor/various/content-only-lock.spec.js +++ b/test/e2e/specs/editor/various/content-only-lock.spec.js @@ -24,8 +24,9 @@ test.describe( 'Content-only lock', () => { ` ); await pageUtils.pressKeys( 'secondary+M' ); - await page.waitForSelector( 'iframe[name="editor-canvas"]' ); - await editor.canvas.click( 'role=document[name="Block: Paragraph"i]' ); + await editor.canvas + .locator( 'role=document[name="Block: Paragraph"i]' ) + .click(); await page.keyboard.type( ' World' ); expect( await editor.getEditedPostContent() ).toMatchSnapshot(); } ); @@ -49,8 +50,9 @@ test.describe( 'Content-only lock', () => { ` ); await pageUtils.pressKeys( 'secondary+M' ); - await page.waitForSelector( 'iframe[name="editor-canvas"]' ); - await editor.canvas.click( 'role=document[name="Block: Paragraph"i]' ); + await editor.canvas + .locator( 'role=document[name="Block: Paragraph"i]' ) + .click(); await page.keyboard.type( ' WP' ); await expect.poll( editor.getBlocks ).toMatchObject( [ { diff --git a/test/e2e/specs/editor/various/copy-cut-paste.spec.js b/test/e2e/specs/editor/various/copy-cut-paste.spec.js index 823926c1121a02..4c249349243669 100644 --- a/test/e2e/specs/editor/various/copy-cut-paste.spec.js +++ b/test/e2e/specs/editor/various/copy-cut-paste.spec.js @@ -13,7 +13,9 @@ test.describe( 'Copy/cut/paste', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Copy - collapsed selection' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '2' ); @@ -32,13 +34,9 @@ test.describe( 'Copy/cut/paste', () => { pageUtils, } ) => { // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.switchToLegacyCanvas(); + + await page.locator( 'role=button[name="Add default block"i]' ).click(); await page.keyboard.type( 'Cut - collapsed selection' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '2' ); @@ -66,7 +64,9 @@ test.describe( 'Copy/cut/paste', () => { await page.evaluate( () => { window.wp.data.dispatch( 'core/block-editor' ).clearSelectedBlock(); } ); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await pageUtils.pressKeys( 'primary+v' ); expect( await editor.getEditedPostContent() ).toMatchSnapshot(); } ); @@ -85,7 +85,9 @@ test.describe( 'Copy/cut/paste', () => { await page.evaluate( () => { window.wp.data.dispatch( 'core/block-editor' ).clearSelectedBlock(); } ); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await pageUtils.pressKeys( 'primary+v' ); expect( await editor.getEditedPostContent() ).toMatchSnapshot(); } ); @@ -95,7 +97,9 @@ test.describe( 'Copy/cut/paste', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'First block' ); await page.keyboard.press( 'Enter' ); @@ -247,7 +251,9 @@ test.describe( 'Copy/cut/paste', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'A block' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'B block' ); @@ -259,9 +265,13 @@ test.describe( 'Copy/cut/paste', () => { await pageUtils.pressKeys( 'primary+ArrowLeft' ); // Sometimes the caret has not moved to the correct position before pressing Enter. // @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887 - await editor.canvas.waitForFunction( - () => window.getSelection().type === 'Caret' - ); + await expect + .poll( async () => + editor.canvas + .locator( ':root' ) + .evaluate( () => window.getSelection().type ) + ) + .toBe( 'Caret' ); // Create a new block at the top of the document to paste there. await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'ArrowUp' ); @@ -274,7 +284,9 @@ test.describe( 'Copy/cut/paste', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'A block' ); await editor.insertBlock( { name: 'core/spacer' } ); await page.keyboard.press( 'Enter' ); @@ -287,9 +299,13 @@ test.describe( 'Copy/cut/paste', () => { await pageUtils.pressKeys( 'primary+ArrowLeft' ); // Sometimes the caret has not moved to the correct position before pressing Enter. // @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887 - await editor.canvas.waitForFunction( - () => window.getSelection().type === 'Caret' - ); + await expect + .poll( async () => + editor.canvas + .locator( ':root' ) + .evaluate( () => window.getSelection().type ) + ) + .toBe( 'Caret' ); // Create a new block at the top of the document to paste there. await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'ArrowUp' ); @@ -302,7 +318,9 @@ test.describe( 'Copy/cut/paste', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'A block' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'B block' ); @@ -314,9 +332,13 @@ test.describe( 'Copy/cut/paste', () => { await pageUtils.pressKeys( 'primary+ArrowLeft' ); // Sometimes the caret has not moved to the correct position before pressing Enter. // @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887 - await editor.canvas.waitForFunction( - () => window.getSelection().type === 'Caret' - ); + await expect + .poll( async () => + editor.canvas + .locator( ':root' ) + .evaluate( () => window.getSelection().type ) + ) + .toBe( 'Caret' ); // Create a new block at the top of the document to paste there. await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'ArrowUp' ); @@ -329,7 +351,9 @@ test.describe( 'Copy/cut/paste', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'A block' ); await editor.insertBlock( { name: 'core/spacer' } ); await page.keyboard.press( 'Enter' ); @@ -342,9 +366,13 @@ test.describe( 'Copy/cut/paste', () => { await pageUtils.pressKeys( 'primary+ArrowLeft' ); // Sometimes the caret has not moved to the correct position before pressing Enter. // @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887 - await editor.canvas.waitForFunction( - () => window.getSelection().type === 'Caret' - ); + await expect + .poll( async () => + editor.canvas + .locator( ':root' ) + .evaluate( () => window.getSelection().type ) + ) + .toBe( 'Caret' ); // Create a new block at the top of the document to paste there. await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'ArrowUp' ); @@ -369,9 +397,13 @@ test.describe( 'Copy/cut/paste', () => { await pageUtils.pressKeys( 'primary+ArrowLeft' ); // Sometimes the caret has not moved to the correct position before pressing Enter. // @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887 - await editor.canvas.waitForFunction( - () => window.getSelection().type === 'Caret' - ); + await expect + .poll( async () => + editor.canvas + .locator( ':root' ) + .evaluate( () => window.getSelection().type ) + ) + .toBe( 'Caret' ); // Create a new block at the top of the document to paste there. await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'ArrowUp' ); @@ -396,9 +428,13 @@ test.describe( 'Copy/cut/paste', () => { await pageUtils.pressKeys( 'primary+ArrowLeft' ); // Sometimes the caret has not moved to the correct position before pressing Enter. // @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887 - await editor.canvas.waitForFunction( - () => window.getSelection().type === 'Caret' - ); + await expect + .poll( async () => + editor.canvas + .locator( ':root' ) + .evaluate( () => window.getSelection().type ) + ) + .toBe( 'Caret' ); // Create a new code block to paste there. await editor.insertBlock( { name: 'core/code' } ); await pageUtils.pressKeys( 'primary+v' ); @@ -420,9 +456,9 @@ test.describe( 'Copy/cut/paste', () => { await pageUtils.pressKeys( 'primary+v' ); // Expect the span to be filtered out. expect( - await editor.canvas.evaluate( - () => document.activeElement.innerHTML - ) + await editor.canvas + .locator( ':root' ) + .evaluate( () => document.activeElement.innerHTML ) ).toMatchSnapshot(); } ); @@ -440,9 +476,9 @@ test.describe( 'Copy/cut/paste', () => { // Ensure the selection is correct. await page.keyboard.type( 'y' ); expect( - await editor.canvas.evaluate( - () => document.activeElement.innerHTML - ) + await editor.canvas + .locator( ':root' ) + .evaluate( () => document.activeElement.innerHTML ) ).toBe( 'axyb' ); } ); diff --git a/test/e2e/specs/editor/various/draggable-blocks.spec.js b/test/e2e/specs/editor/various/draggable-blocks.spec.js index 2d95e3bdefe975..fb56b43dc6e031 100644 --- a/test/e2e/specs/editor/various/draggable-blocks.spec.js +++ b/test/e2e/specs/editor/various/draggable-blocks.spec.js @@ -42,9 +42,9 @@ test.describe( 'Draggable block', () => {

2

` ); - await editor.canvas.focus( - 'role=document[name="Block: Paragraph"i] >> text=2' - ); + await editor.canvas + .locator( 'role=document[name="Block: Paragraph"i] >> text=2' ) + .focus(); await editor.showBlockToolbar(); const dragHandle = page.locator( @@ -114,9 +114,9 @@ test.describe( 'Draggable block', () => {

2

` ); - await editor.canvas.focus( - 'role=document[name="Block: Paragraph"i] >> text=1' - ); + await editor.canvas + .locator( 'role=document[name="Block: Paragraph"i] >> text=1' ) + .focus(); await editor.showBlockToolbar(); const dragHandle = page.locator( @@ -197,9 +197,9 @@ test.describe( 'Draggable block', () => { ], } ); - await editor.canvas.focus( - 'role=document[name="Block: Paragraph"i] >> text=2' - ); + await editor.canvas + .locator( 'role=document[name="Block: Paragraph"i] >> text=2' ) + .focus(); await editor.showBlockToolbar(); const dragHandle = page.locator( @@ -278,9 +278,9 @@ test.describe( 'Draggable block', () => { ], } ); - await editor.canvas.focus( - 'role=document[name="Block: Paragraph"i] >> text=1' - ); + await editor.canvas + .locator( 'role=document[name="Block: Paragraph"i] >> text=1' ) + .focus(); await editor.showBlockToolbar(); const dragHandle = page.locator( @@ -342,13 +342,6 @@ test.describe( 'Draggable block', () => { editor, pageUtils, } ) => { - // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); // Insert a row. await editor.insertBlock( { name: 'core/group', @@ -421,7 +414,7 @@ test.describe( 'Draggable block', () => { 'Dragging over the empty group block but outside the appender should still show the blue background' ).toHaveCSS( 'background-color', 'rgb(0, 124, 186)' ); - await drop(); + await drop( rowBlock ); await expect( rowAppender ).toBeHidden(); await expect.poll( editor.getBlocks ).toMatchObject( [ { @@ -453,7 +446,7 @@ test.describe( 'Draggable block', () => { 'rgb(0, 124, 186)' ); - await drop(); + await drop( columnAppender ); await expect( columnAppender ).toBeHidden(); await expect.poll( editor.getBlocks ).toMatchObject( [ { name: 'core/group' }, diff --git a/test/e2e/specs/editor/various/font-size-picker.spec.js b/test/e2e/specs/editor/various/font-size-picker.spec.js index ddc47e3ee6de66..5c6cb4b186e25e 100644 --- a/test/e2e/specs/editor/various/font-size-picker.spec.js +++ b/test/e2e/specs/editor/various/font-size-picker.spec.js @@ -24,9 +24,9 @@ test.describe( 'Font Size Picker', () => { page, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Paragraph to be made "small"' ); await page.click( 'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]' @@ -47,9 +47,9 @@ test.describe( 'Font Size Picker', () => { pageUtils, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Paragraph reset - custom size' ); await page.click( 'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]' @@ -139,9 +139,9 @@ test.describe( 'Font Size Picker', () => { pageUtils, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Paragraph to be made "large"' ); await page.click( 'role=group[name="Font size"i] >> role=button[name="Font size"i]' @@ -161,9 +161,9 @@ test.describe( 'Font Size Picker', () => { pageUtils, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Paragraph with font size reset using tools panel menu' ); @@ -194,9 +194,9 @@ test.describe( 'Font Size Picker', () => { pageUtils, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Paragraph with font size reset using input field' ); @@ -231,9 +231,9 @@ test.describe( 'Font Size Picker', () => { page, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Paragraph to be made "large"' ); await page.click( 'role=radiogroup[name="Font size"i] >> role=radio[name="Large"i]' @@ -250,9 +250,9 @@ test.describe( 'Font Size Picker', () => { page, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Paragraph with font size reset using tools panel menu' ); @@ -281,9 +281,9 @@ test.describe( 'Font Size Picker', () => { pageUtils, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( - 'role=button[name="Add default block"i]' - ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Paragraph with font size reset using input field' ); diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js index e3aa17c5a101cb..14a2fc653e3873 100644 --- a/test/e2e/specs/editor/various/footnotes.spec.js +++ b/test/e2e/specs/editor/various/footnotes.spec.js @@ -27,7 +27,9 @@ test.describe( 'Footnotes', () => { } ); test( 'can be inserted', async ( { editor, page } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'first paragraph' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'second paragraph' ); @@ -38,7 +40,7 @@ test.describe( 'Footnotes', () => { await page.keyboard.type( 'first footnote' ); - const id1 = await editor.canvas.evaluate( () => { + const id1 = await editor.canvas.locator( ':root' ).evaluate( () => { return document.activeElement.id; } ); @@ -58,7 +60,7 @@ test.describe( 'Footnotes', () => { }, ] ); - await editor.canvas.click( 'p:text("first paragraph")' ); + await editor.canvas.locator( 'p:text("first paragraph")' ).click(); await editor.showBlockToolbar(); await editor.clickBlockToolbarButton( 'More' ); @@ -66,7 +68,7 @@ test.describe( 'Footnotes', () => { await page.keyboard.type( 'second footnote' ); - const id2 = await editor.canvas.evaluate( () => { + const id2 = await editor.canvas.locator( ':root' ).evaluate( () => { return document.activeElement.id; } ); @@ -99,7 +101,7 @@ test.describe( 'Footnotes', () => { }, ] ); - await editor.canvas.click( 'p:text("first paragraph")' ); + await editor.canvas.locator( 'p:text("first paragraph")' ).click(); await editor.showBlockToolbar(); await editor.clickBlockToolbarButton( 'Move down' ); @@ -133,7 +135,7 @@ test.describe( 'Footnotes', () => { }, ] ); - await editor.canvas.click( `a[href="#${ id2 }-link"]` ); + await editor.canvas.locator( `a[href="#${ id2 }-link"]` ).click(); await page.keyboard.press( 'Backspace' ); expect( await editor.getBlocks() ).toMatchObject( [ @@ -161,7 +163,7 @@ test.describe( 'Footnotes', () => { }, ] ); - await editor.canvas.click( `a[href="#${ id1 }-link"]` ); + await editor.canvas.locator( `a[href="#${ id1 }-link"]` ).click(); await page.keyboard.press( 'Backspace' ); expect( await editor.getBlocks() ).toMatchObject( [ @@ -186,14 +188,16 @@ test.describe( 'Footnotes', () => { } ); test( 'can be inserted in a list', async ( { editor, page } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '* 1' ); await editor.clickBlockToolbarButton( 'More' ); await page.locator( 'button:text("Footnote")' ).click(); await page.keyboard.type( 'a' ); - const id1 = await editor.canvas.evaluate( () => { + const id1 = await editor.canvas.locator( ':root' ).evaluate( () => { return document.activeElement.id; } ); @@ -224,7 +228,9 @@ test.describe( 'Footnotes', () => { test( 'can be inserted in a table', async ( { editor, page } ) => { await editor.insertBlock( { name: 'core/table' } ); - await editor.canvas.click( 'role=button[name="Create Table"i]' ); + await editor.canvas + .locator( 'role=button[name="Create Table"i]' ) + .click(); await page.keyboard.type( '1' ); await editor.showBlockToolbar(); await editor.clickBlockToolbarButton( 'More' ); @@ -232,7 +238,7 @@ test.describe( 'Footnotes', () => { await page.keyboard.type( 'a' ); - const id1 = await editor.canvas.evaluate( () => { + const id1 = await editor.canvas.locator( ':root' ).evaluate( () => { return document.activeElement.id; } ); @@ -282,7 +288,9 @@ test.describe( 'Footnotes', () => { } ); test( 'works with revisions', async ( { editor, page } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'first paragraph' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'second paragraph' ); @@ -294,11 +302,11 @@ test.describe( 'Footnotes', () => { // Check if content is correctly slashed on save and restore. await page.keyboard.type( 'first footnote"' ); - const id1 = await editor.canvas.evaluate( () => { + const id1 = await editor.canvas.locator( ':root' ).evaluate( () => { return document.activeElement.id; } ); - await editor.canvas.click( 'p:text("first paragraph")' ); + await editor.canvas.locator( 'p:text("first paragraph")' ).click(); await editor.showBlockToolbar(); await editor.clickBlockToolbarButton( 'More' ); @@ -306,7 +314,7 @@ test.describe( 'Footnotes', () => { await page.keyboard.type( 'second footnote' ); - const id2 = await editor.canvas.evaluate( () => { + const id2 = await editor.canvas.locator( ':root' ).evaluate( () => { return document.activeElement.id; } ); @@ -322,7 +330,7 @@ test.describe( 'Footnotes', () => { }, ] ); - await editor.canvas.click( 'p:text("first paragraph")' ); + await editor.canvas.locator( 'p:text("first paragraph")' ).click(); await editor.showBlockToolbar(); await editor.clickBlockToolbarButton( 'Move down' ); @@ -348,7 +356,7 @@ test.describe( 'Footnotes', () => { await previewPage.close(); await editorPage.bringToFront(); - await editor.canvas.click( 'p:text("first paragraph")' ); + await editor.canvas.locator( 'p:text("first paragraph")' ).click(); // Open revisions. await editor.openDocumentSettingsSidebar(); @@ -383,7 +391,9 @@ test.describe( 'Footnotes', () => { } ); test( 'can be previewed when published', async ( { editor, page } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'a' ); await editor.showBlockToolbar(); @@ -396,7 +406,7 @@ test.describe( 'Footnotes', () => { const postId = await editor.publishPost(); // Test previewing changes to meta. - await editor.canvas.click( 'ol.wp-block-footnotes li span' ); + await editor.canvas.locator( 'ol.wp-block-footnotes li span' ).click(); await page.keyboard.press( 'End' ); await page.keyboard.type( '2' ); @@ -410,8 +420,9 @@ test.describe( 'Footnotes', () => { await previewPage.close(); await editorPage.bringToFront(); - // Test again, this time with an existing revision (different code path). - await editor.canvas.click( 'ol.wp-block-footnotes li span' ); + // Test again, this time with an existing revision (different code + // path). + await editor.canvas.locator( 'ol.wp-block-footnotes li span' ).click(); await page.keyboard.press( 'End' ); // Test slashing. await page.keyboard.type( '3"' ); diff --git a/test/e2e/specs/editor/various/inner-blocks-templates.spec.js b/test/e2e/specs/editor/various/inner-blocks-templates.spec.js index 47ef0dfe747912..0f9aed33d0773b 100644 --- a/test/e2e/specs/editor/various/inner-blocks-templates.spec.js +++ b/test/e2e/specs/editor/various/inner-blocks-templates.spec.js @@ -28,11 +28,9 @@ test.describe( 'Inner blocks templates', () => { name: 'test/test-inner-blocks-async-template', } ); - const blockWithTemplateContent = page - .frameLocator( '[name=editor-canvas]' ) - .locator( - 'role=document[name="Block: Test Inner Blocks Async Template"i] >> text=OneTwo' - ); + const blockWithTemplateContent = editor.canvas.locator( + 'role=document[name="Block: Test Inner Blocks Async Template"i] >> text=OneTwo' + ); // The block template content appears asynchronously, so wait for it. await expect( blockWithTemplateContent ).toBeVisible(); diff --git a/test/e2e/specs/editor/various/inserting-blocks.spec.js b/test/e2e/specs/editor/various/inserting-blocks.spec.js index cfef686ef5c879..a48fe117c97a2c 100644 --- a/test/e2e/specs/editor/various/inserting-blocks.spec.js +++ b/test/e2e/specs/editor/various/inserting-blocks.spec.js @@ -31,7 +31,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { ); await admin.createNewPost(); - await insertingBlocksUtils.runWithoutIframe(); + await editor.switchToLegacyCanvas(); // We need a dummy block in place to display the drop indicator due to a bug. // @see https://github.com/WordPress/gutenberg/issues/44064 @@ -39,7 +39,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { name: 'core/paragraph', attributes: { content: 'Dummy text' }, } ); - const paragraphBlock = editor.canvas.locator( + const paragraphBlock = page.locator( '[data-type="core/paragraph"] >> text=Dummy text' ); @@ -109,7 +109,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { insertingBlocksUtils, } ) => { await admin.createNewPost(); - await insertingBlocksUtils.runWithoutIframe(); + await editor.switchToLegacyCanvas(); // We need a dummy block in place to display the drop indicator due to a bug. // @see https://github.com/WordPress/gutenberg/issues/44064 @@ -120,7 +120,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { const beforeContent = await editor.getEditedPostContent(); - const paragraphBlock = editor.canvas.locator( + const paragraphBlock = page.locator( '[data-type="core/paragraph"] >> text=Dummy text' ); @@ -175,7 +175,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { ); await admin.createNewPost(); - await insertingBlocksUtils.runWithoutIframe(); + await editor.switchToLegacyCanvas(); // We need a dummy block in place to display the drop indicator due to a bug. // @see https://github.com/WordPress/gutenberg/issues/44064 @@ -184,7 +184,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { attributes: { content: 'Dummy text' }, } ); - const paragraphBlock = editor.canvas.locator( + const paragraphBlock = page.locator( '[data-type="core/paragraph"] >> text=Dummy text' ); @@ -245,7 +245,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { insertingBlocksUtils, } ) => { await admin.createNewPost(); - await insertingBlocksUtils.runWithoutIframe(); + await editor.switchToLegacyCanvas(); // We need a dummy block in place to display the drop indicator due to a bug. // @see https://github.com/WordPress/gutenberg/issues/44064 @@ -256,7 +256,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { const beforeContent = await editor.getEditedPostContent(); - const paragraphBlock = editor.canvas.locator( + const paragraphBlock = page.locator( '[data-type="core/paragraph"] >> text=Dummy text' ); @@ -307,10 +307,8 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { admin, page, editor, - insertingBlocksUtils, } ) => { await admin.createNewPost(); - await insertingBlocksUtils.runWithoutIframe(); const inserterButton = page.getByRole( 'button', { name: 'Toggle block inserter', @@ -390,16 +388,4 @@ class InsertingBlocksUtils { 'data-testid=block-draggable-chip >> visible=true' ); } - - async runWithoutIframe() { - /** - * @todo Some drag an drop tests are failing, so run them without iframe for now. - */ - return await this.page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); - } } diff --git a/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js b/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js index 8d1f37187fee18..57b958fdfc4b44 100644 --- a/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js +++ b/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js @@ -13,7 +13,9 @@ test.describe( 'Keep styles on block transforms', () => { editor, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '## Heading' ); await page.click( 'role=button[name="Color Text styles"i]' ); await page.click( 'role=option[name="Color: Luminous vivid orange"i]' ); @@ -37,15 +39,10 @@ test.describe( 'Keep styles on block transforms', () => { pageUtils, editor, } ) => { - // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Line 1 to be made large' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'Line 2 to be made large' ); @@ -78,7 +75,9 @@ test.describe( 'Keep styles on block transforms', () => { editor, } ) => { await editor.openDocumentSettingsSidebar(); - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Line 1 to be made large' ); await page.click( 'role=radio[name="Large"i]' ); await editor.showBlockToolbar(); diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js index 7fd21085deae2a..222d743acdf395 100644 --- a/test/e2e/specs/editor/various/list-view.spec.js +++ b/test/e2e/specs/editor/various/list-view.spec.js @@ -261,13 +261,6 @@ test.describe( 'List View', () => { page, pageUtils, } ) => { - // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); await editor.insertBlock( { name: 'core/image' } ); await editor.insertBlock( { name: 'core/paragraph', diff --git a/test/e2e/specs/editor/various/mentions.spec.js b/test/e2e/specs/editor/various/mentions.spec.js index 061b8d67a0801f..fef3b1c3e3d2e1 100644 --- a/test/e2e/specs/editor/various/mentions.spec.js +++ b/test/e2e/specs/editor/various/mentions.spec.js @@ -23,7 +23,9 @@ test.describe( 'autocomplete mentions', () => { } ); test( 'should insert mention', async ( { page, editor } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'I am @ad' ); await expect( page.locator( 'role=listbox >> role=option[name=/admin/i]' ) @@ -42,7 +44,9 @@ test.describe( 'autocomplete mentions', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Stuck in the middle with you' ); await pageUtils.pressKeys( 'ArrowLeft', { times: 'you'.length } ); await page.keyboard.type( '@j' ); @@ -62,7 +66,9 @@ test.describe( 'autocomplete mentions', () => { page, editor, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'I am @j' ); await expect( page.locator( 'role=listbox >> role=option[name=/testuser/i]' ) diff --git a/test/e2e/specs/editor/various/multi-block-selection.spec.js b/test/e2e/specs/editor/various/multi-block-selection.spec.js index 3374a13b98eb79..32fe45e6951bcc 100644 --- a/test/e2e/specs/editor/various/multi-block-selection.spec.js +++ b/test/e2e/specs/editor/various/multi-block-selection.spec.js @@ -248,20 +248,14 @@ test.describe( 'Multi-block selection', () => { multiBlockSelectionUtils, } ) => { // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); - await editor.canvas - .getByRole( 'button', { name: 'Add default block' } ) - .click(); + await editor.switchToLegacyCanvas(); + + await page.getByRole( 'button', { name: 'Add default block' } ).click(); await page.keyboard.type( '1' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '2' ); - await editor.canvas + await page .getByRole( 'document', { name: 'Block: Paragraph' } ) .filter( { hasText: '1' } ) .click( { modifiers: [ 'Shift' ] } ); @@ -278,11 +272,11 @@ test.describe( 'Multi-block selection', () => { .getByRole( 'toolbar', { name: 'Block tools' } ) .getByRole( 'button', { name: 'Group' } ) .click(); - await editor.canvas + await page .getByRole( 'document', { name: 'Block: Paragraph' } ) .filter( { hasText: '1' } ) .click(); - await editor.canvas + await page .getByRole( 'document', { name: 'Block: Paragraph' } ) .filter( { hasText: '2' } ) .click( { modifiers: [ 'Shift' ] } ); @@ -300,12 +294,8 @@ test.describe( 'Multi-block selection', () => { pageUtils, } ) => { // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); + await editor.switchToLegacyCanvas(); + await editor.insertBlock( { name: 'core/paragraph', attributes: { content: 'test' }, @@ -317,16 +307,12 @@ test.describe( 'Multi-block selection', () => { .getByRole( 'button', { name: 'Dismiss this notice' } ) .filter( { hasText: 'Draft saved' } ) ).toBeVisible(); + await page.reload(); // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); + await editor.switchToLegacyCanvas(); - await editor.canvas + await page .getByRole( 'document', { name: 'Block: Paragraph' } ) .click( { modifiers: [ 'Shift' ] } ); await pageUtils.pressKeys( 'primary+a' ); @@ -573,37 +559,42 @@ test.describe( 'Multi-block selection', () => { page, editor, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '12' ); await page.keyboard.press( 'ArrowLeft' ); - const [ coord1, coord2 ] = await editor.canvas.evaluate( () => { - const selection = window.getSelection(); - - if ( ! selection.rangeCount ) { - return; - } - - const range = selection.getRangeAt( 0 ); - const rect1 = range.getClientRects()[ 0 ]; - const element = document.querySelector( - '[data-type="core/paragraph"]' - ); - const rect2 = element.getBoundingClientRect(); - const iframeOffset = window.frameElement.getBoundingClientRect(); - - return [ - { - x: iframeOffset.x + rect1.x, - y: iframeOffset.y + rect1.y + rect1.height / 2, - }, - { - // Move a bit outside the paragraph. - x: iframeOffset.x + rect2.x - 5, - y: iframeOffset.y + rect2.y + rect2.height / 2, - }, - ]; - } ); + const [ coord1, coord2 ] = await editor.canvas + .locator( ':root' ) + .evaluate( () => { + const selection = window.getSelection(); + + if ( ! selection.rangeCount ) { + return; + } + + const range = selection.getRangeAt( 0 ); + const rect1 = range.getClientRects()[ 0 ]; + const element = document.querySelector( + '[data-type="core/paragraph"]' + ); + const rect2 = element.getBoundingClientRect(); + const iframeOffset = + window.frameElement.getBoundingClientRect(); + + return [ + { + x: iframeOffset.x + rect1.x, + y: iframeOffset.y + rect1.y + rect1.height / 2, + }, + { + // Move a bit outside the paragraph. + x: iframeOffset.x + rect2.x - 5, + y: iframeOffset.y + rect2.y + rect2.height / 2, + }, + ]; + } ); await page.mouse.click( coord1.x, coord1.y ); await page.mouse.down(); @@ -935,7 +926,9 @@ test.describe( 'Multi-block selection', () => { .toEqual( [] ); await expect .poll( () => - editor.canvas.evaluate( () => window.getSelection().toString() ) + editor.canvas + .locator( ':root' ) + .evaluate( () => window.getSelection().toString() ) ) .toBe( 'Post title' ); } ); @@ -1184,12 +1177,8 @@ test.describe( 'Multi-block selection', () => { editor, } ) => { // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); + await editor.switchToLegacyCanvas(); + await editor.insertBlock( { name: 'core/paragraph', attributes: { content: '1[' }, @@ -1199,25 +1188,29 @@ test.describe( 'Multi-block selection', () => { attributes: { content: ']2' }, } ); // Focus and move the caret to the end. - await editor.canvas + const secondParagraphBlock = page .getByRole( 'document', { name: 'Block: Paragraph' } ) - .filter( { hasText: ']2' } ) - .click(); + .filter( { hasText: ']2' } ); + const secondParagraphBlockBox = + await secondParagraphBlock.boundingBox(); + await secondParagraphBlock.click( { + position: { + x: secondParagraphBlockBox.width - 1, + y: secondParagraphBlockBox.height / 2, + }, + } ); await page.keyboard.press( 'ArrowLeft' ); - const strongText = editor.canvas + const strongText = page .getByRole( 'region', { name: 'Editor content' } ) .getByText( '1', { exact: true } ); const strongBox = await strongText.boundingBox(); // Focus and move the caret to the end. - await editor.canvas - .getByRole( 'document', { name: 'Block: Paragraph' } ) - .filter( { hasText: '1[' } ) - .click( { - // Ensure clicking on the right half of the element. - position: { x: strongBox.width - 1, y: strongBox.height / 2 }, - modifiers: [ 'Shift' ], - } ); + await strongText.click( { + // Ensure clicking on the right half of the element. + position: { x: strongBox.width - 1, y: strongBox.height / 2 }, + modifiers: [ 'Shift' ], + } ); await page.keyboard.press( 'Backspace' ); // Ensure selection is in the correct place. @@ -1354,9 +1347,9 @@ class MultiBlockSelectionUtils { * Tests if the native selection matches the block selection. */ assertNativeSelection = async () => { - const selection = await this.#editor.canvas.evaluateHandle( () => - window.getSelection() - ); + const selection = await this.#editor.canvas + .locator( ':root' ) + .evaluateHandle( () => window.getSelection() ); const { isMultiSelected, selectionStart, selectionEnd } = await this.#page.evaluate( () => { diff --git a/test/e2e/specs/editor/various/new-post.spec.js b/test/e2e/specs/editor/various/new-post.spec.js index 4b192693c07b07..9d6f4ef45d9db1 100644 --- a/test/e2e/specs/editor/various/new-post.spec.js +++ b/test/e2e/specs/editor/various/new-post.spec.js @@ -74,10 +74,9 @@ test.describe( 'new editor state', () => { await admin.createNewPost(); // Enter a title for this post. - await editor.canvas.type( - 'role=textbox[name="Add title"i]', - 'Here is the title' - ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .type( 'Here is the title' ); // Save the post as a draft. await page.click( 'role=button[name="Save draft"i]' ); await page.waitForSelector( diff --git a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js index 5d63ff789e4ec7..629a437a41665b 100644 --- a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js +++ b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js @@ -156,7 +156,6 @@ class PostEditorTemplateMode { async createPostAndSaveDraft() { await this.admin.createNewPost(); - await this.editor.canvas.waitForLoadState(); // Create a random post. await this.page.keyboard.type( 'Just an FSE Post' ); await this.page.keyboard.press( 'Enter' ); diff --git a/test/e2e/specs/editor/various/post-visibility.spec.js b/test/e2e/specs/editor/various/post-visibility.spec.js index 3f83221c27b819..365209ef2e4e55 100644 --- a/test/e2e/specs/editor/various/post-visibility.spec.js +++ b/test/e2e/specs/editor/various/post-visibility.spec.js @@ -78,7 +78,9 @@ test.describe( 'Post visibility', () => { await admin.createNewPost(); // Enter a title for this post. - await editor.canvas.type( 'role=textbox[name="Add title"i]', 'Title' ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .type( 'Title' ); await editor.openDocumentSettingsSidebar(); diff --git a/test/e2e/specs/editor/various/preview.spec.js b/test/e2e/specs/editor/various/preview.spec.js index a3fa4544f39f84..cfec384adba9bf 100644 --- a/test/e2e/specs/editor/various/preview.spec.js +++ b/test/e2e/specs/editor/various/preview.spec.js @@ -27,10 +27,9 @@ test.describe( 'Preview', () => { editorPage.locator( 'role=button[name="Preview"i]' ) ).toBeDisabled(); - await editor.canvas.type( - 'role=textbox[name="Add title"i]', - 'Hello World' - ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .type( 'Hello World' ); const previewPage = await editor.openPreviewPage( editorPage ); const previewTitle = previewPage.locator( 'role=heading[level=1]' ); @@ -48,7 +47,9 @@ test.describe( 'Preview', () => { // Return to editor to change title. await editorPage.bringToFront(); - await editor.canvas.type( 'role=textbox[name="Add title"i]', '!' ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .type( '!' ); await previewUtils.waitForPreviewNavigation( previewPage ); // Title in preview should match updated input. @@ -70,10 +71,9 @@ test.describe( 'Preview', () => { // Return to editor to change title. await editorPage.bringToFront(); - await editor.canvas.fill( - 'role=textbox[name="Add title"i]', - 'Hello World! And more.' - ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .fill( 'Hello World! And more.' ); await previewUtils.waitForPreviewNavigation( previewPage ); // Title in preview should match updated input. @@ -109,7 +109,9 @@ test.describe( 'Preview', () => { const editorPage = page; // Type aaaaa in the title field. - await editor.canvas.type( 'role=textbox[name="Add title"]', 'aaaaa' ); + await editor.canvas + .locator( 'role=textbox[name="Add title"]' ) + .type( 'aaaaa' ); await editorPage.keyboard.press( 'Tab' ); // Save the post as a draft. @@ -129,10 +131,9 @@ test.describe( 'Preview', () => { await editorPage.bringToFront(); // Append bbbbb to the title, and tab away from the title so blur event is triggered. - await editor.canvas.fill( - 'role=textbox[name="Add title"i]', - 'aaaaabbbbb' - ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .fill( 'aaaaabbbbb' ); await editorPage.keyboard.press( 'Tab' ); // Save draft and open the preview page right after. @@ -157,7 +158,9 @@ test.describe( 'Preview', () => { const editorPage = page; // Type Lorem in the title field. - await editor.canvas.type( 'role=textbox[name="Add title"i]', 'Lorem' ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .type( 'Lorem' ); // Open the preview page. const previewPage = await editor.openPreviewPage( editorPage ); @@ -174,7 +177,9 @@ test.describe( 'Preview', () => { await page.click( 'role=button[name="Close panel"i]' ); // Change the title and preview again. - await editor.canvas.type( 'role=textbox[name="Add title"i]', ' Ipsum' ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .type( ' Ipsum' ); await previewUtils.waitForPreviewNavigation( previewPage ); // Title in preview should match updated input. @@ -196,7 +201,9 @@ test.describe( 'Preview', () => { ).toBeVisible(); // Change the title. - await editor.canvas.type( 'role=textbox[name="Add title"i]', ' Draft' ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .type( ' Draft' ); // Open the preview page. await previewUtils.waitForPreviewNavigation( previewPage ); @@ -227,10 +234,9 @@ test.describe( 'Preview with Custom Fields enabled', () => { const editorPage = page; // Add an initial title and content. - await editor.canvas.type( - 'role=textbox[name="Add title"i]', - 'title 1' - ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .type( 'title 1' ); await editor.insertBlock( { name: 'core/paragraph', attributes: { content: 'content 1' }, @@ -254,14 +260,12 @@ test.describe( 'Preview with Custom Fields enabled', () => { // Return to editor and modify the title and content. await editorPage.bringToFront(); - await editor.canvas.fill( - 'role=textbox[name="Add title"i]', - 'title 2' - ); - await editor.canvas.fill( - 'role=document >> text="content 1"', - 'content 2' - ); + await editor.canvas + .locator( 'role=textbox[name="Add title"i]' ) + .fill( 'title 2' ); + await editor.canvas + .locator( 'role=document >> text="content 1"' ) + .fill( 'content 2' ); // Open the preview page. await previewUtils.waitForPreviewNavigation( previewPage ); diff --git a/test/e2e/specs/editor/various/rich-text.spec.js b/test/e2e/specs/editor/various/rich-text.spec.js index 4ebd2cd450e70b..2969a33d254852 100644 --- a/test/e2e/specs/editor/various/rich-text.spec.js +++ b/test/e2e/specs/editor/various/rich-text.spec.js @@ -35,7 +35,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'test' ); await pageUtils.pressKeys( 'primary+a' ); await pageUtils.pressKeys( 'primary+b' ); @@ -53,7 +55,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Some ' ); await pageUtils.pressKeys( 'primary+b' ); await page.keyboard.type( 'bold' ); @@ -73,7 +77,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await pageUtils.pressKeys( 'primary+b' ); await pageUtils.pressKeys( 'primary+i' ); await page.keyboard.type( '1' ); @@ -102,11 +108,14 @@ test.describe( 'RichText', () => { await pageUtils.pressKeys( 'shift+ArrowLeft' ); await pageUtils.pressKeys( 'primary+b' ); - const count = await editor.canvas.evaluate( - () => - document.querySelectorAll( '*[data-rich-text-format-boundary]' ) - .length - ); + const count = await editor.canvas + .locator( ':root' ) + .evaluate( + () => + document.querySelectorAll( + '*[data-rich-text-format-boundary]' + ).length + ); expect( count ).toBe( 1 ); } ); @@ -114,7 +123,9 @@ test.describe( 'RichText', () => { page, editor, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'Some ' ); await editor.clickBlockToolbarButton( 'Bold' ); await page.keyboard.type( 'bold' ); @@ -134,7 +145,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'A `backtick`' ); expect( await editor.getBlocks() ).toMatchObject( [ @@ -155,7 +168,9 @@ test.describe( 'RichText', () => { page, editor, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '`a`' ); await page.keyboard.press( 'Backspace' ); @@ -171,7 +186,9 @@ test.describe( 'RichText', () => { page, editor, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '`a`' ); await page.keyboard.type( 'b' ); await page.keyboard.press( 'Backspace' ); @@ -184,7 +201,9 @@ test.describe( 'RichText', () => { page, editor, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '`a`' ); await page.evaluate( () => new Promise( window.requestIdleCallback ) ); // Move inside format boundary. @@ -200,7 +219,9 @@ test.describe( 'RichText', () => { page, editor, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'A `backtick` and more.' ); expect( await editor.getBlocks() ).toMatchObject( [ @@ -216,7 +237,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'A selection test.' ); await page.keyboard.press( 'Home' ); await page.keyboard.press( 'ArrowRight' ); @@ -246,14 +269,16 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1' ); await pageUtils.pressKeys( 'primary+b' ); await page.keyboard.type( '2' ); await pageUtils.pressKeys( 'primary+b' ); await page.keyboard.type( '3' ); - await editor.canvas.evaluate( () => { + await editor.canvas.locator( ':root' ).evaluate( () => { let called; const { body } = document; const config = { @@ -313,7 +338,7 @@ test.describe( 'RichText', () => { await page.keyboard.type( '4' ); - await editor.canvas.evaluate( () => { + await editor.canvas.locator( ':root' ).evaluate( () => { // The selection change event should be called once. If there's only // one item in `window.unsubscribes`, it means that only one // function is present to disconnect the `mutationObserver`. @@ -339,7 +364,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await pageUtils.pressKeys( 'primary+b' ); await page.keyboard.type( '1' ); await pageUtils.pressKeys( 'primary+b' ); @@ -369,7 +396,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await pageUtils.pressKeys( 'primary+b' ); await page.keyboard.type( '12' ); await pageUtils.pressKeys( 'primary+b' ); @@ -391,7 +420,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1' ); await page.keyboard.press( 'Tab' ); await pageUtils.pressKeys( 'shift+Tab' ); @@ -412,11 +443,13 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1' ); // Simulate moving focus to a different app, then moving focus back, // without selection being changed. - await editor.canvas.evaluate( () => { + await editor.canvas.locator( ':root' ).evaluate( () => { const activeElement = document.activeElement; activeElement.blur(); activeElement.focus(); @@ -443,7 +476,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '2' ); @@ -479,7 +514,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '2' ); await pageUtils.pressKeys( 'primary+a' ); await pageUtils.pressKeys( 'primary+x' ); @@ -500,7 +537,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1' ); await pageUtils.pressKeys( 'primary+b' ); await page.keyboard.type( '2' ); @@ -525,7 +564,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1' ); await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'Backspace' ); @@ -545,7 +586,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await pageUtils.pressKeys( 'primary+b' ); await page.keyboard.type( '1' ); await page.keyboard.type( '2' ); @@ -571,7 +614,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); // Add text and select to color. await page.keyboard.type( '1' ); @@ -626,7 +671,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); // Create two lines of text in a paragraph. await page.keyboard.type( '1' ); await pageUtils.pressKeys( 'shift+Enter' ); @@ -668,7 +715,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); // Create an indented list of two lines. await page.keyboard.type( '* 1' ); @@ -717,7 +766,9 @@ test.describe( 'RichText', () => { } ); test( 'should navigate arround emoji', async ( { page, editor } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '🍓' ); // Only one press on arrow left should be required to move in front of // the emoji. @@ -735,12 +786,14 @@ test.describe( 'RichText', () => { test( 'should run input rules after composition end', async ( { editor, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); // Playwright doesn't support composition, so emulate it by inserting // text in the DOM directly, setting selection in the right place, and // firing `compositionend`. // See https://github.com/puppeteer/puppeteer/issues/4981. - await editor.canvas.evaluate( async () => { + await editor.canvas.locator( ':root' ).evaluate( async () => { document.activeElement.textContent = '`a`'; const selection = window.getSelection(); // The `selectionchange` and `compositionend` events should run in separate event @@ -767,7 +820,9 @@ test.describe( 'RichText', () => { editor, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await pageUtils.pressKeys( 'primary+b' ); await page.keyboard.type( '1' ); await pageUtils.pressKeys( 'primary+b' ); diff --git a/test/e2e/specs/editor/various/rtl.spec.js b/test/e2e/specs/editor/various/rtl.spec.js index 8475605e339fcb..aaf1186cc5abae 100644 --- a/test/e2e/specs/editor/various/rtl.spec.js +++ b/test/e2e/specs/editor/various/rtl.spec.js @@ -150,7 +150,9 @@ test.describe( 'RTL', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await pageUtils.pressKeys( 'primary+b' ); await page.keyboard.type( ARABIC_ONE ); await pageUtils.pressKeys( 'primary+b' ); diff --git a/test/e2e/specs/editor/various/splitting-merging.spec.js b/test/e2e/specs/editor/various/splitting-merging.spec.js index 1c5e12be8abb11..29e7e5d64522c9 100644 --- a/test/e2e/specs/editor/various/splitting-merging.spec.js +++ b/test/e2e/specs/editor/various/splitting-merging.spec.js @@ -4,8 +4,11 @@ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); test.describe( 'splitting and merging blocks (@firefox, @webkit)', () => { - test.beforeEach( async ( { admin } ) => { + test.beforeEach( async ( { admin, editor } ) => { await admin.createNewPost(); + await expect( + editor.canvas.getByRole( 'textbox', { name: 'Add title' } ) + ).toBeFocused(); } ); test.afterEach( async ( { requestUtils } ) => { diff --git a/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js b/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js index 23186dfff2de96..e706dfc3607dc3 100644 --- a/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js +++ b/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js @@ -79,7 +79,9 @@ test.describe( 'Toolbar roving tabindex', () => { // Move focus to the first toolbar item. await page.keyboard.press( 'Home' ); await ToolbarRovingTabindexUtils.expectLabelToHaveFocus( 'Table' ); - await editor.canvas.click( `role=button[name="Create Table"i]` ); + await editor.canvas + .locator( `role=button[name="Create Table"i]` ) + .click(); await pageUtils.pressKeys( 'Tab' ); await ToolbarRovingTabindexUtils.testBlockToolbarKeyboardNavigation( 'Body cell text', diff --git a/test/e2e/specs/editor/various/undo.spec.js b/test/e2e/specs/editor/various/undo.spec.js index dfdbef60ffaa9c..51683997aaf6f0 100644 --- a/test/e2e/specs/editor/various/undo.spec.js +++ b/test/e2e/specs/editor/various/undo.spec.js @@ -20,7 +20,9 @@ test.describe( 'undo', () => { pageUtils, undoUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'before pause' ); await editor.page.waitForTimeout( 1000 ); await page.keyboard.type( ' after pause' ); @@ -88,7 +90,9 @@ test.describe( 'undo', () => { pageUtils, undoUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'before keyboard ' ); await pageUtils.pressKeys( 'primary+b' ); @@ -160,7 +164,9 @@ test.describe( 'undo', () => { } ); test( 'should undo bold', async ( { page, pageUtils, editor } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'test' ); await page.click( 'role=button[name="Save draft"i]' ); await expect( @@ -169,8 +175,7 @@ test.describe( 'undo', () => { ) ).toBeVisible(); await page.reload(); - await page.waitForSelector( 'iframe[name="editor-canvas"]' ); - await editor.canvas.click( '[data-type="core/paragraph"]' ); + await editor.canvas.locator( '[data-type="core/paragraph"]' ).click(); await pageUtils.pressKeys( 'primary+a' ); await pageUtils.pressKeys( 'primary+b' ); await pageUtils.pressKeys( 'primary+z' ); @@ -190,7 +195,9 @@ test.describe( 'undo', () => { pageUtils, undoUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); const firstBlock = await editor.getEditedPostContent(); @@ -333,7 +340,9 @@ test.describe( 'undo', () => { // See: https://github.com/WordPress/gutenberg/issues/14950 // Issue is demonstrated from an edited post: create, save, and reload. - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( 'original' ); await page.click( 'role=button[name="Save draft"i]' ); await expect( @@ -342,11 +351,12 @@ test.describe( 'undo', () => { ) ).toBeVisible(); await page.reload(); - await page.waitForSelector( 'iframe[name="editor-canvas"]' ); // Issue is demonstrated by forcing state merges (multiple inputs) on // an existing text after a fresh reload. - await editor.canvas.click( '[data-type="core/paragraph"] >> nth=0' ); + await editor.canvas + .locator( '[data-type="core/paragraph"] >> nth=0' ) + .click(); await page.keyboard.type( 'modified' ); // The issue is demonstrated after the one second delay to trigger the @@ -370,7 +380,9 @@ test.describe( 'undo', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1' ); await page.click( 'role=button[name="Save draft"i]' ); await expect( @@ -388,7 +400,9 @@ test.describe( 'undo', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1' ); await editor.publishPost(); await pageUtils.pressKeys( 'primary+z' ); @@ -401,7 +415,9 @@ test.describe( 'undo', () => { page, pageUtils, } ) => { - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( '1' ); await page.click( 'role=button[name="Save draft"i]' ); @@ -416,7 +432,7 @@ test.describe( 'undo', () => { await expect( page.locator( 'role=button[name="Undo"]' ) ).toBeDisabled(); - await editor.canvas.click( '[data-type="core/paragraph"]' ); + await editor.canvas.locator( '[data-type="core/paragraph"]' ).click(); await page.keyboard.type( '2' ); @@ -446,7 +462,9 @@ test.describe( 'undo', () => { // block attribute as in the previous action and results in transient edits // and skipping `undo` history steps. const text = 'tonis'; - await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); await page.keyboard.type( text ); await editor.publishPost(); await pageUtils.pressKeys( 'primary+z' ); diff --git a/test/e2e/specs/editor/various/writing-flow.spec.js b/test/e2e/specs/editor/various/writing-flow.spec.js index 81bbcbbc758b93..98bb00a596f03c 100644 --- a/test/e2e/specs/editor/various/writing-flow.spec.js +++ b/test/e2e/specs/editor/various/writing-flow.spec.js @@ -10,8 +10,11 @@ test.use( { } ); test.describe( 'Writing Flow (@firefox, @webkit)', () => { - test.beforeEach( async ( { admin } ) => { + test.beforeEach( async ( { admin, editor } ) => { await admin.createNewPost(); + await expect( + editor.canvas.getByRole( 'textbox', { name: 'Add title' } ) + ).toBeFocused(); } ); test.afterAll( async ( { requestUtils } ) => { @@ -46,9 +49,11 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { .poll( writingFlowUtils.getActiveBlockName ) .toBe( 'core/column' ); await page.keyboard.press( 'ArrowUp' ); - const activeElementBlockType = await editor.canvas.evaluate( () => - document.activeElement.getAttribute( 'data-type' ) - ); + const activeElementBlockType = await editor.canvas + .locator( ':root' ) + .evaluate( () => + document.activeElement.getAttribute( 'data-type' ) + ); expect( activeElementBlockType ).toBe( 'core/columns' ); await expect .poll( writingFlowUtils.getActiveBlockName ) @@ -515,12 +520,12 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { } ) => { await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'Enter' ); - await editor.canvas.evaluate( () => { + await editor.canvas.locator( ':root' ).evaluate( () => { document.activeElement.style.paddingTop = '100px'; } ); await page.keyboard.press( 'ArrowUp' ); await page.keyboard.type( '1' ); - await editor.canvas.evaluate( () => { + await editor.canvas.locator( ':root' ).evaluate( () => { document.activeElement.style.paddingBottom = '100px'; } ); await page.keyboard.press( 'ArrowDown' ); @@ -544,7 +549,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { } ) => { await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'Enter' ); - await editor.canvas.evaluate( () => { + await editor.canvas.locator( ':root' ).evaluate( () => { document.activeElement.style.lineHeight = 'normal'; } ); await page.keyboard.press( 'ArrowUp' ); @@ -745,7 +750,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { } ) => { await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'Enter' ); - await editor.canvas.evaluate( () => { + await editor.canvas.locator( ':root' ).evaluate( () => { document.activeElement.style.paddingLeft = '100px'; } ); await page.keyboard.press( 'Enter' ); @@ -1015,9 +1020,9 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { await page.keyboard.type( 'a' ); async function getHeight() { - return await editor.canvas.evaluate( - () => document.activeElement.offsetHeight - ); + return await editor.canvas + .locator( ':root' ) + .evaluate( () => document.activeElement.offsetHeight ); } const height = await getHeight(); @@ -1049,9 +1054,9 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { await page.keyboard.type( 'a' ); async function getHeight() { - return await editor.canvas.evaluate( - () => document.activeElement.offsetHeight - ); + return await editor.canvas + .locator( ':root' ) + .evaluate( () => document.activeElement.offsetHeight ); } const height = await getHeight(); @@ -1084,9 +1089,9 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { await page.keyboard.type( 'a' ); async function getHeight() { - return await editor.canvas.evaluate( - () => document.activeElement.offsetHeight - ); + return await editor.canvas + .locator( ':root' ) + .evaluate( () => document.activeElement.offsetHeight ); } const height = await getHeight(); @@ -1135,19 +1140,23 @@ class WritingFlowUtils { await this.page.keyboard.press( 'Enter' ); await this.page.keyboard.type( '/columns' ); await this.page.keyboard.press( 'Enter' ); - await this.editor.canvas.click( - 'role=button[name="Two columns; equal split"i]' - ); - await this.editor.canvas.click( 'role=button[name="Add block"i]' ); + await this.editor.canvas + .locator( 'role=button[name="Two columns; equal split"i]' ) + .click(); + await this.editor.canvas + .locator( 'role=button[name="Add block"i]' ) + .click(); await this.page.click( 'role=listbox[name="Blocks"i] >> role=option[name="Paragraph"i]' ); await this.page.keyboard.type( '1st col' ); // If this text is too long, it may wrap to a new line and cause test failure. That's why we're using "1st" instead of "First" here. - await this.editor.canvas.focus( - 'role=document[name="Block: Column (2 of 2)"i]' - ); - await this.editor.canvas.click( 'role=button[name="Add block"i]' ); + await this.editor.canvas + .locator( 'role=document[name="Block: Column (2 of 2)"i]' ) + .focus(); + await this.editor.canvas + .locator( 'role=button[name="Add block"i]' ) + .click(); await this.page.click( 'role=listbox[name="Blocks"i] >> role=option[name="Paragraph"i]' ); diff --git a/test/e2e/specs/interactivity/directive-init.spec.ts b/test/e2e/specs/interactivity/directive-init.spec.ts index aa81ab1ea61db2..e18341a48fa3f4 100644 --- a/test/e2e/specs/interactivity/directive-init.spec.ts +++ b/test/e2e/specs/interactivity/directive-init.spec.ts @@ -53,7 +53,7 @@ test.describe( 'data-wp-init', () => { await toggle.click(); - await expect( show ).not.toBeVisible(); + await expect( show ).toBeHidden(); await expect( isMounted ).toHaveText( 'false' ); } ); @@ -65,7 +65,7 @@ test.describe( 'data-wp-init', () => { await toggle.click(); - await expect( show ).not.toBeVisible(); + await expect( show ).toBeHidden(); await expect( isMounted ).toHaveText( 'false' ); await toggle.click(); diff --git a/test/e2e/specs/interactivity/directive-slots.spec.ts b/test/e2e/specs/interactivity/directive-slots.spec.ts index d93e50f767215f..195af52fdb1bd2 100644 --- a/test/e2e/specs/interactivity/directive-slots.spec.ts +++ b/test/e2e/specs/interactivity/directive-slots.spec.ts @@ -108,7 +108,7 @@ test.describe( 'data-wp-slot', () => { await page.getByTestId( 'slot-5-button' ).click(); await expect( fillContainer ).toBeEmpty(); - await expect( slot5 ).not.toBeVisible(); + await expect( slot5 ).toBeHidden(); await expect( slots.getByTestId( 'fill' ) ).toBeVisible(); await expect( slots.locator( 'css= > *' ) ).toHaveText( [ '[1]', diff --git a/test/e2e/specs/site-editor/block-removal.spec.js b/test/e2e/specs/site-editor/block-removal.spec.js index c1d6cdfea00222..5c46ac769efd3f 100644 --- a/test/e2e/specs/site-editor/block-removal.spec.js +++ b/test/e2e/specs/site-editor/block-removal.spec.js @@ -17,7 +17,7 @@ test.describe( 'Site editor block removal prompt', () => { postId: 'emptytheme//index', postType: 'wp_template', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); } ); test( 'should appear when attempting to remove Query Block', async ( { diff --git a/test/e2e/specs/site-editor/command-center.spec.js b/test/e2e/specs/site-editor/command-center.spec.js index 9d22248bc2362a..60c5ec30b1247f 100644 --- a/test/e2e/specs/site-editor/command-center.spec.js +++ b/test/e2e/specs/site-editor/command-center.spec.js @@ -19,6 +19,7 @@ test.describe( 'Site editor command palette', () => { test( 'Open the command palette and navigate to the page create page', async ( { page, + editor, } ) => { await page .getByRole( 'button', { name: 'Open command palette' } ) @@ -26,13 +27,11 @@ test.describe( 'Site editor command palette', () => { await page.keyboard.press( 'Meta+k' ); await page.keyboard.type( 'new page' ); await page.getByRole( 'option', { name: 'Add new page' } ).click(); - await page.waitForSelector( 'iframe[name="editor-canvas"]' ); - const frame = page.frame( 'editor-canvas' ); await expect( page ).toHaveURL( '/wp-admin/post-new.php?post_type=page' ); await expect( - frame.getByRole( 'textbox', { name: 'Add title' } ) + editor.canvas.getByRole( 'textbox', { name: 'Add title' } ) ).toBeVisible(); } ); diff --git a/test/e2e/specs/site-editor/font-library.spec.js b/test/e2e/specs/site-editor/font-library.spec.js index e6521cb7c540bc..6aca027a30e788 100644 --- a/test/e2e/specs/site-editor/font-library.spec.js +++ b/test/e2e/specs/site-editor/font-library.spec.js @@ -14,7 +14,7 @@ test.describe( 'Font Library', () => { postId: 'emptytheme//index', postType: 'wp_template', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); } ); test( 'should display the "Manage Fonts" icon', async ( { page } ) => { @@ -39,7 +39,7 @@ test.describe( 'Font Library', () => { postId: 'twentytwentythree//index', postType: 'wp_template', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); } ); test( 'should display the "Manage Fonts" icon', async ( { page } ) => { diff --git a/test/e2e/specs/site-editor/global-styles-sidebar.spec.js b/test/e2e/specs/site-editor/global-styles-sidebar.spec.js index 02717f1b194327..f255640cb2f12f 100644 --- a/test/e2e/specs/site-editor/global-styles-sidebar.spec.js +++ b/test/e2e/specs/site-editor/global-styles-sidebar.spec.js @@ -17,7 +17,7 @@ test.describe( 'Global styles sidebar', () => { postId: 'emptytheme//index', postType: 'wp_template', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); } ); test( 'should filter blocks list results', async ( { page } ) => { diff --git a/test/e2e/specs/site-editor/hybrid-theme.spec.js b/test/e2e/specs/site-editor/hybrid-theme.spec.js index 060daf508491aa..7c9f3fc3a5e232 100644 --- a/test/e2e/specs/site-editor/hybrid-theme.spec.js +++ b/test/e2e/specs/site-editor/hybrid-theme.spec.js @@ -40,7 +40,7 @@ test.describe( 'Hybrid theme', () => { page.getByRole( 'region', { name: 'Editor content' } ) ).toBeVisible(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await expect( editor.canvas.getByRole( 'document', { diff --git a/test/e2e/specs/site-editor/iframe-rendering.spec.js b/test/e2e/specs/site-editor/iframe-rendering.spec.js index 02389cc936f06c..4391f134a9f80b 100644 --- a/test/e2e/specs/site-editor/iframe-rendering.spec.js +++ b/test/e2e/specs/site-editor/iframe-rendering.spec.js @@ -14,16 +14,15 @@ test.describe( 'Site editor iframe rendering mode', () => { test( 'Should render editor in standards mode.', async ( { admin, - page, + editor, } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', postType: 'wp_template', } ); - - const compatMode = await page - .locator( 'iframe[name="editor-canvas"]' ) - .evaluate( ( iframe ) => iframe.contentDocument.compatMode ); + const compatMode = await editor.canvas + .locator( ':root' ) + .evaluate( () => document.compatMode ); // CSS1Compat = expected standards mode. // BackCompat = quirks mode. diff --git a/test/e2e/specs/site-editor/list-view.spec.js b/test/e2e/specs/site-editor/list-view.spec.js index 3057feaf7772b0..ed64574168bd02 100644 --- a/test/e2e/specs/site-editor/list-view.spec.js +++ b/test/e2e/specs/site-editor/list-view.spec.js @@ -18,7 +18,7 @@ test.describe( 'Site Editor List View', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); } ); test( 'should open by default when preference is enabled', async ( { @@ -105,7 +105,7 @@ test.describe( 'Site Editor List View', () => { // Since focus is now inside the list view, the shortcut should close // the sidebar. await pageUtils.pressKeys( 'access+o' ); - await expect( listView ).not.toBeVisible(); + await expect( listView ).toBeHidden(); // Focus should now be on the list view toggle button. await expect( @@ -129,7 +129,7 @@ test.describe( 'Site Editor List View', () => { } ) ).toBeFocused(); await pageUtils.pressKeys( 'access+o' ); - await expect( listView ).not.toBeVisible(); + await expect( listView ).toBeHidden(); await expect( page.getByRole( 'button', { name: 'List View' } ) ).toBeFocused(); diff --git a/test/e2e/specs/site-editor/multi-entity-saving.spec.js b/test/e2e/specs/site-editor/multi-entity-saving.spec.js index b4d2f1dbda0d03..bb062aafa3e7f3 100644 --- a/test/e2e/specs/site-editor/multi-entity-saving.spec.js +++ b/test/e2e/specs/site-editor/multi-entity-saving.spec.js @@ -23,7 +23,7 @@ test.describe( 'Site Editor - Multi-entity save flow', () => { postId: 'emptytheme//index', postType: 'wp_template', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); } ); test( 'save flow should work as expected', async ( { editor, page } ) => { diff --git a/test/e2e/specs/site-editor/push-to-global-styles.spec.js b/test/e2e/specs/site-editor/push-to-global-styles.spec.js index 937b2aea0fb454..9507245c192d2f 100644 --- a/test/e2e/specs/site-editor/push-to-global-styles.spec.js +++ b/test/e2e/specs/site-editor/push-to-global-styles.spec.js @@ -17,7 +17,7 @@ test.describe( 'Push to Global Styles button', () => { postId: 'emptytheme//index', postType: 'wp_template', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); } ); test( 'should apply Heading block styles to all Heading blocks', async ( { diff --git a/test/e2e/specs/site-editor/site-editor-inserter.spec.js b/test/e2e/specs/site-editor/site-editor-inserter.spec.js index f8ab0a534d8580..98cb8e4e74149c 100644 --- a/test/e2e/specs/site-editor/site-editor-inserter.spec.js +++ b/test/e2e/specs/site-editor/site-editor-inserter.spec.js @@ -18,7 +18,7 @@ test.describe( 'Site Editor Inserter', () => { test.beforeEach( async ( { admin, editor } ) => { await admin.visitSiteEditor(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); } ); test( 'inserter toggle button should toggle global inserter', async ( { diff --git a/test/e2e/specs/site-editor/style-book.spec.js b/test/e2e/specs/site-editor/style-book.spec.js index eeaa1270f3ff9e..08f5e6463ebc7f 100644 --- a/test/e2e/specs/site-editor/style-book.spec.js +++ b/test/e2e/specs/site-editor/style-book.spec.js @@ -20,7 +20,7 @@ test.describe( 'Style Book', () => { test.beforeEach( async ( { admin, editor, styleBook, page } ) => { await admin.visitSiteEditor(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await styleBook.open(); await expect( page.locator( 'role=region[name="Style Book"i]' ) @@ -30,16 +30,16 @@ test.describe( 'Style Book', () => { test( 'should disable toolbar buttons when open', async ( { page } ) => { await expect( page.locator( 'role=button[name="Toggle block inserter"i]' ) - ).not.toBeVisible(); + ).toBeHidden(); await expect( page.locator( 'role=button[name="Tools"i]' ) - ).not.toBeVisible(); + ).toBeHidden(); await expect( page.locator( 'role=button[name="Undo"i]' ) - ).not.toBeVisible(); + ).toBeHidden(); await expect( page.locator( 'role=button[name="Redo"i]' ) - ).not.toBeVisible(); + ).toBeHidden(); await expect( page.locator( 'role=button[name="View"i]' ) ).toBeDisabled(); @@ -139,7 +139,7 @@ test.describe( 'Style Book', () => { await expect( styleBookRegion, 'should close when close button is clicked' - ).not.toBeVisible(); + ).toBeHidden(); // Open Style Book again. await page.getByRole( 'button', { name: 'Style Book' } ).click(); @@ -153,7 +153,7 @@ test.describe( 'Style Book', () => { await expect( styleBookRegion, 'should close when Escape key is pressed' - ).not.toBeVisible(); + ).toBeHidden(); } ); } ); diff --git a/test/e2e/specs/site-editor/style-variations.spec.js b/test/e2e/specs/site-editor/style-variations.spec.js index ee71e856269b04..8868c733006687 100644 --- a/test/e2e/specs/site-editor/style-variations.spec.js +++ b/test/e2e/specs/site-editor/style-variations.spec.js @@ -39,7 +39,7 @@ test.describe( 'Global styles variations', () => { postId: 'gutenberg-test-themes/style-variations//index', postType: 'wp_template', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await siteEditorStyleVariations.browseStyles(); @@ -76,7 +76,7 @@ test.describe( 'Global styles variations', () => { postId: 'gutenberg-test-themes/style-variations//index', postType: 'wp_template', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await siteEditorStyleVariations.browseStyles(); await page.click( 'role=button[name="pink"i]' ); await page.click( @@ -117,7 +117,7 @@ test.describe( 'Global styles variations', () => { postId: 'gutenberg-test-themes/style-variations//index', postType: 'wp_template', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await siteEditorStyleVariations.browseStyles(); await page.click( 'role=button[name="yellow"i]' ); await page.click( @@ -164,7 +164,7 @@ test.describe( 'Global styles variations', () => { postId: 'gutenberg-test-themes/style-variations//index', postType: 'wp_template', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await siteEditorStyleVariations.browseStyles(); await page.click( 'role=button[name="pink"i]' ); await page.click( @@ -196,15 +196,16 @@ test.describe( 'Global styles variations', () => { postId: 'gutenberg-test-themes/style-variations//index', postType: 'wp_template', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await siteEditorStyleVariations.browseStyles(); await page.click( 'role=button[name="yellow"i]' ); - const frame = page.frame( 'editor-canvas' ); - const paragraph = frame.locator( 'text="My awesome paragraph"' ); + const paragraph = editor.canvas.locator( + 'text="My awesome paragraph"' + ); await expect( paragraph ).toHaveCSS( 'color', 'rgb(25, 25, 17)' ); - const body = frame.locator( 'css=body' ); + const body = editor.canvas.locator( 'css=body' ); await expect( body ).toHaveCSS( 'background-color', 'rgb(255, 239, 11)' diff --git a/test/e2e/specs/site-editor/template-part.spec.js b/test/e2e/specs/site-editor/template-part.spec.js index cd81a616b1fee1..d1c215ec2a4949 100644 --- a/test/e2e/specs/site-editor/template-part.spec.js +++ b/test/e2e/specs/site-editor/template-part.spec.js @@ -28,11 +28,13 @@ test.describe( 'Template Part', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); // Insert a new template block and 'start blank'. await editor.insertBlock( { name: 'core/template-part' } ); - await editor.canvas.click( 'role=button[name="Start blank"i]' ); + await editor.canvas + .locator( 'role=button[name="Start blank"i]' ) + .click(); // Fill in a name in the dialog that pops up. await page.type( 'role=dialog >> role=textbox[name="Name"i]', 'New' ); @@ -56,7 +58,7 @@ test.describe( 'Template Part', () => { } ) => { // Visit the index. await admin.visitSiteEditor(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); const headerTemplateParts = editor.canvas.locator( '[data-type="core/template-part"]' ); @@ -66,7 +68,7 @@ test.describe( 'Template Part', () => { // Insert a new template block and choose an existing header pattern. await editor.insertBlock( { name: 'core/template-part' } ); - await editor.canvas.click( 'role=button[name="Choose"i]' ); + await editor.canvas.locator( 'role=button[name="Choose"i]' ).click(); await page.click( 'role=listbox[name="Block Patterns"i] >> role=option[name="header"i]' ); @@ -83,7 +85,7 @@ test.describe( 'Template Part', () => { const paragraphText = 'Test 2'; await admin.visitSiteEditor(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); // Add a block and select it. await editor.insertBlock( { name: 'core/paragraph', @@ -108,7 +110,7 @@ test.describe( 'Template Part', () => { // Check that the header contains the paragraph added earlier. const templatePartWithParagraph = editor.canvas.locator( '[data-type="core/template-part"]', - { has: paragraphBlock } + { hasText: paragraphText } ); await expect( templatePartWithParagraph ).toBeVisible(); @@ -123,7 +125,7 @@ test.describe( 'Template Part', () => { const paragraphText2 = 'Test 4'; await admin.visitSiteEditor(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); // Add a block and select it. await editor.insertBlock( { name: 'core/paragraph', @@ -158,11 +160,11 @@ test.describe( 'Template Part', () => { // Check that the header contains the paragraph added earlier. const templatePartWithParagraph1 = editor.canvas.locator( '[data-type="core/template-part"]', - { has: paragraphBlock1 } + { hasText: paragraphText1 } ); const templatePartWithParagraph2 = editor.canvas.locator( '[data-type="core/template-part"]', - { has: paragraphBlock2 } + { hasText: paragraphText2 } ); // TODO: I couldn't find an easy way to assert that the same template @@ -185,7 +187,7 @@ test.describe( 'Template Part', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await editor.insertBlock( { name: 'core/paragraph', attributes: { @@ -196,14 +198,14 @@ test.describe( 'Template Part', () => { // Visit the index. await admin.visitSiteEditor(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); // Check that the header contains the paragraph added earlier. const paragraph = editor.canvas.locator( `p >> text="${ paragraphText }"` ); const templatePartWithParagraph = editor.canvas.locator( '[data-type="core/template-part"]', - { has: paragraph } + { hasText: paragraphText } ); await expect( templatePartWithParagraph ).toBeVisible(); @@ -215,7 +217,7 @@ test.describe( 'Template Part', () => { // There should be a paragraph but no header template part. await expect( paragraph ).toBeVisible(); - await expect( templatePartWithParagraph ).not.toBeVisible(); + await expect( templatePartWithParagraph ).toBeHidden(); } ); test( 'shows changes in a template when a template part it contains is modified', async ( { @@ -228,7 +230,7 @@ test.describe( 'Template Part', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); // Edit the header. await editor.insertBlock( { name: 'core/paragraph', @@ -241,7 +243,7 @@ test.describe( 'Template Part', () => { // Visit the index. await admin.visitSiteEditor(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); const paragraph = editor.canvas.locator( `p >> text="${ paragraphText }"` ); @@ -261,7 +263,7 @@ test.describe( 'Template Part', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await editor.insertBlock( { name: 'core/paragraph', attributes: { @@ -300,7 +302,7 @@ test.describe( 'Template Part', () => { page, } ) => { await admin.visitSiteEditor(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); // Add a block and select it. await editor.insertBlock( { @@ -341,7 +343,7 @@ test.describe( 'Template Part', () => { page, } ) => { await admin.visitSiteEditor(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); // Select existing header template part. await editor.selectBlocks( @@ -355,7 +357,7 @@ test.describe( 'Template Part', () => { // Verify that the widget area import button is not there. await expect( page.getByRole( 'combobox', { name: 'Import widget area' } ) - ).not.toBeVisible(); + ).toBeHidden(); } ); test( 'Keeps focus in place on undo in template parts', async ( { @@ -368,7 +370,7 @@ test.describe( 'Template Part', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); // Select the site title block. const siteTitle = editor.canvas.getByRole( 'document', { diff --git a/test/e2e/specs/site-editor/template-revert.spec.js b/test/e2e/specs/site-editor/template-revert.spec.js index c8edc034901a2b..c281b71d16a183 100644 --- a/test/e2e/specs/site-editor/template-revert.spec.js +++ b/test/e2e/specs/site-editor/template-revert.spec.js @@ -23,7 +23,7 @@ test.describe( 'Template Revert', () => { test.beforeEach( async ( { admin, requestUtils, editor } ) => { await requestUtils.deleteAllTemplates( 'wp_template' ); await admin.visitSiteEditor(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); } ); test( 'should delete the template after saving the reverted template', async ( { @@ -55,7 +55,7 @@ test.describe( 'Template Revert', () => { page.locator( 'role=region[name="Editor settings"i] >> role=button[name="Actions"i]' ) - ).not.toBeVisible(); + ).toBeHidden(); } ); test( 'should show the original content after revert', async ( { @@ -277,7 +277,7 @@ test.describe( 'Template Revert', () => { await editor.saveSiteEditorEntities(); await admin.visitSiteEditor(); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); const contentAfter = await templateRevertUtils.getCurrentSiteEditorContent(); expect( contentAfter ).toEqual( contentBefore ); diff --git a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js index a7ecebfd9ed443..61fbf7c795a60b 100644 --- a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js +++ b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js @@ -31,7 +31,7 @@ test.describe( 'Global styles revisions', () => { editor, userGlobalStylesRevisions, } ) => { - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); const currentRevisions = await userGlobalStylesRevisions.getGlobalStylesRevisions(); await userGlobalStylesRevisions.openStylesPanel(); @@ -66,7 +66,7 @@ test.describe( 'Global styles revisions', () => { editor, userGlobalStylesRevisions, } ) => { - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await userGlobalStylesRevisions.openStylesPanel(); await page.getByRole( 'button', { name: 'Colors styles' } ).click(); await page @@ -110,7 +110,7 @@ test.describe( 'Global styles revisions', () => { editor, userGlobalStylesRevisions, } ) => { - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); await userGlobalStylesRevisions.openStylesPanel(); await userGlobalStylesRevisions.openRevisions(); const lastRevisionButton = page diff --git a/test/e2e/specs/site-editor/writing-flow.spec.js b/test/e2e/specs/site-editor/writing-flow.spec.js index f9681a5ea2d46f..8ee6ce0e565572 100644 --- a/test/e2e/specs/site-editor/writing-flow.spec.js +++ b/test/e2e/specs/site-editor/writing-flow.spec.js @@ -24,7 +24,7 @@ test.describe( 'Site editor writing flow', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); // Select the first site title block. const siteTitleBlock = editor.canvas.locator( 'role=document[name="Block: Site Title"i]' @@ -52,7 +52,7 @@ test.describe( 'Site editor writing flow', () => { postId: 'emptytheme//header', postType: 'wp_template_part', } ); - await editor.canvas.click( 'body' ); + await editor.canvas.locator( 'body' ).click(); // Make sure the sidebar is open. await editor.openDocumentSettingsSidebar(); diff --git a/test/e2e/specs/widgets/customizing-widgets.spec.js b/test/e2e/specs/widgets/customizing-widgets.spec.js index e771c92fc24eef..6a6a51cae26860 100644 --- a/test/e2e/specs/widgets/customizing-widgets.spec.js +++ b/test/e2e/specs/widgets/customizing-widgets.spec.js @@ -36,11 +36,7 @@ test.describe( 'Widgets Customizer', () => { await requestUtils.activateTheme( 'twentytwentyone' ); } ); - test( 'should add blocks', async ( { - page, - widgetsCustomizerPage, - editor, - } ) => { + test( 'should add blocks', async ( { page, widgetsCustomizerPage } ) => { const previewFrame = widgetsCustomizerPage.previewFrame; await widgetsCustomizerPage.visitCustomizerPage(); @@ -86,9 +82,11 @@ test.describe( 'Widgets Customizer', () => { await page.click( 'role=option[name="Search"i]' ); - await editor.canvas.focus( - 'role=document[name="Block: Search"i] >> role=textbox[name="Label text"i]' - ); + await page + .locator( + 'role=document[name="Block: Search"i] >> role=textbox[name="Label text"i]' + ) + .focus(); await page.keyboard.type( 'My ' ); @@ -233,7 +231,6 @@ test.describe( 'Widgets Customizer', () => { page, requestUtils, widgetsCustomizerPage, - editor, } ) => { await requestUtils.addWidgetBlock( `\n

First Paragraph

\n`, @@ -282,7 +279,7 @@ test.describe( 'Widgets Customizer', () => { await headingWidget.click(); // noop click on the widget text to unfocus the editor and hide toolbar await editHeadingWidget.click(); - const headingBlock = editor.canvas.locator( + const headingBlock = page.locator( 'role=document[name="Block: Heading"i] >> text="First Heading"' ); await expect( headingBlock ).toBeFocused(); @@ -586,13 +583,12 @@ test.describe( 'Widgets Customizer', () => { test( 'preserves content in the Custom HTML block', async ( { page, widgetsCustomizerPage, - editor, } ) => { await widgetsCustomizerPage.visitCustomizerPage(); await widgetsCustomizerPage.expandWidgetArea( 'Footer #1' ); await widgetsCustomizerPage.addBlock( 'Custom HTML' ); - const HTMLBlockTextarea = editor.canvas.locator( + const HTMLBlockTextarea = page.locator( 'role=document[name="Block: Custom HTML"i] >> role=textbox[name="HTML"i]' ); await HTMLBlockTextarea.type( 'hello' ); diff --git a/test/integration/fixtures/blocks/core__form-input.html b/test/integration/fixtures/blocks/core__form-input.html new file mode 100644 index 00000000000000..718c592641bc32 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input.html @@ -0,0 +1,3 @@ + + + diff --git a/test/integration/fixtures/blocks/core__form-input.json b/test/integration/fixtures/blocks/core__form-input.json new file mode 100644 index 00000000000000..33802bbcc2088d --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input.json @@ -0,0 +1,12 @@ +[ + { + "name": "core/missing", + "isValid": true, + "attributes": { + "originalName": "core/form-input", + "originalUndelimitedContent": "", + "originalContent": "\n\n" + }, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__form-input.parsed.json b/test/integration/fixtures/blocks/core__form-input.parsed.json new file mode 100644 index 00000000000000..73058fc2e17f0b --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input.parsed.json @@ -0,0 +1,14 @@ +[ + { + "blockName": "core/form-input", + "attrs": { + "label": "Name", + "required": true + }, + "innerBlocks": [], + "innerHTML": "\n\n", + "innerContent": [ + "\n\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__form-input.serialized.html b/test/integration/fixtures/blocks/core__form-input.serialized.html new file mode 100644 index 00000000000000..4e1f6b77998de9 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input.serialized.html @@ -0,0 +1,3 @@ + + + diff --git a/test/integration/fixtures/blocks/core__form.html b/test/integration/fixtures/blocks/core__form.html new file mode 100644 index 00000000000000..ab18e0e11c81a5 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form.html @@ -0,0 +1,21 @@ + +
+ + + + + + + + + + + + + + + + +
+
+ diff --git a/test/integration/fixtures/blocks/core__form.json b/test/integration/fixtures/blocks/core__form.json new file mode 100644 index 00000000000000..ba07b17e4d00c6 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form.json @@ -0,0 +1,63 @@ +[ + { + "name": "core/missing", + "isValid": true, + "attributes": { + "originalName": "core/form", + "originalUndelimitedContent": "
\n\n\n\n\n
\n
", + "originalContent": "\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\n
\n" + }, + "innerBlocks": [ + { + "name": "core/missing", + "isValid": true, + "attributes": { + "originalName": "core/form-input", + "originalUndelimitedContent": "", + "originalContent": "\n\n" + }, + "innerBlocks": [] + }, + { + "name": "core/missing", + "isValid": true, + "attributes": { + "originalName": "core/form-input", + "originalUndelimitedContent": "", + "originalContent": "\n\n" + }, + "innerBlocks": [] + }, + { + "name": "core/missing", + "isValid": true, + "attributes": { + "originalName": "core/form-input", + "originalUndelimitedContent": "", + "originalContent": "\n\n" + }, + "innerBlocks": [] + }, + { + "name": "core/missing", + "isValid": true, + "attributes": { + "originalName": "core/form-input", + "originalUndelimitedContent": "", + "originalContent": "\n\n" + }, + "innerBlocks": [] + }, + { + "name": "core/missing", + "isValid": true, + "attributes": { + "originalName": "core/form-input", + "originalUndelimitedContent": "
", + "originalContent": "\n
\n" + }, + "innerBlocks": [] + } + ] + } +] diff --git a/test/integration/fixtures/blocks/core__form.parsed.json b/test/integration/fixtures/blocks/core__form.parsed.json new file mode 100644 index 00000000000000..379bee84c84e10 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form.parsed.json @@ -0,0 +1,84 @@ +[ + { + "blockName": "core/form", + "attrs": {}, + "innerBlocks": [ + { + "blockName": "core/form-input", + "attrs": { + "label": "Name", + "required": true + }, + "innerBlocks": [], + "innerHTML": "\n\n", + "innerContent": [ + "\n\n" + ] + }, + { + "blockName": "core/form-input", + "attrs": { + "type": "email", + "label": "Email", + "required": true + }, + "innerBlocks": [], + "innerHTML": "\n\n", + "innerContent": [ + "\n\n" + ] + }, + { + "blockName": "core/form-input", + "attrs": { + "type": "url", + "label": "Website" + }, + "innerBlocks": [], + "innerHTML": "\n\n", + "innerContent": [ + "\n\n" + ] + }, + { + "blockName": "core/form-input", + "attrs": { + "type": "textarea", + "label": "Comment", + "required": true + }, + "innerBlocks": [], + "innerHTML": "\n\n", + "innerContent": [ + "\n\n" + ] + }, + { + "blockName": "core/form-input", + "attrs": { + "type": "submit", + "label": "Submit" + }, + "innerBlocks": [], + "innerHTML": "\n
\n", + "innerContent": [ + "\n
\n" + ] + } + ], + "innerHTML": "\n
\n\n\n\n\n\n\n\n
\n", + "innerContent": [ + "\n
", + null, + "\n\n", + null, + "\n\n", + null, + "\n\n", + null, + "\n\n", + null, + "
\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__form.serialized.html b/test/integration/fixtures/blocks/core__form.serialized.html new file mode 100644 index 00000000000000..58a2a49967eb56 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form.serialized.html @@ -0,0 +1,19 @@ + +
+ + + + + + + + + + + + + +
+ +
+ diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js index 3f105886cd2d11..a76889622b4a2f 100644 --- a/tools/webpack/packages.js +++ b/tools/webpack/packages.js @@ -147,6 +147,14 @@ module.exports = { devtoolNamespace: 'wp', filename: './build/[name]/index.min.js', path: join( __dirname, '..', '..' ), + devtoolModuleFilenameTemplate: ( info ) => { + if ( info.resourcePath.includes( '/@wordpress/' ) ) { + const resourcePath = + info.resourcePath.split( '/@wordpress/' )[ 1 ]; + return `../../packages/${ resourcePath }`; + } + return `webpack://${ info.namespace }/${ info.resourcePath }`; + }, }, plugins: [ ...plugins,