From 830d13b9502b9dd32175098342f090f5d46fcb29 Mon Sep 17 00:00:00 2001 From: margolisj <1588194+margolisj@users.noreply.github.com> Date: Thu, 5 Oct 2023 03:31:14 -0400 Subject: [PATCH 01/20] Text Component: Typescript'n (#54953) * No boolean? * Easy type changes and moves styles folder to new location, was clashing in code editor. * Types component * Quick pass on utils and hook * Story to TSX * Adds back boolean type and removes unneeded as string. * First pass on PR comments * Replaces lowercaseProps function with clearer version. * Adds changelog * options type in utils for createHighlighterText converted out of jsDocs * Move native styles back * Removes boolean from adjustLineHeightForInnerControls. * First pass on storybook upgrade * Pass on headings * Exports and imports the correct one. --- packages/components/CHANGELOG.md | 1 + .../src/heading/stories/index.story.tsx | 6 +- packages/components/src/text/README.md | 4 +- .../src/text/{component.js => component.tsx} | 16 ++-- .../components/src/text/{hook.js => hook.ts} | 27 +++---- .../src/text/{index.js => index.ts} | 0 .../src/text/stories/index.story.js | 53 ------------ .../src/text/stories/index.story.tsx | 80 +++++++++++++++++++ .../src/text/{styles.js => styles.ts} | 0 packages/components/src/text/types.ts | 7 +- .../src/text/{utils.js => utils.ts} | 54 +++++++++---- 11 files changed, 148 insertions(+), 100 deletions(-) rename packages/components/src/text/{component.js => component.tsx} (64%) rename packages/components/src/text/{hook.js => hook.ts} (87%) rename packages/components/src/text/{index.js => index.ts} (100%) delete mode 100644 packages/components/src/text/stories/index.story.js create mode 100644 packages/components/src/text/stories/index.story.tsx rename packages/components/src/text/{styles.js => styles.ts} (100%) rename packages/components/src/text/{utils.js => utils.ts} (76%) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 3c8176d2c2962..779c1b4238290 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -34,6 +34,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/heading/stories/index.story.tsx b/packages/components/src/heading/stories/index.story.tsx index e774fd5331273..d82a59f08c825 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/text/README.md b/packages/components/src/text/README.md index 7747ec9cbc727..6b1fc15615840 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 f1ce842dae915..f3fe69d936584 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 5198845c1dae7..a447b2ce5133b 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 b1b4e3f455536..0000000000000 --- 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 0000000000000..f762ca3b4e3ff --- /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 df60f4ee0e2d7..e4702c1f3257d 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 2496c86cca25a..85e41a56c6e34 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, From b3d28f94355054874d9f7c064dd7e8dc1e8f88af Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Thu, 5 Oct 2023 10:44:57 +0300 Subject: [PATCH 02/20] Introduce experimental form & inputs blocks to allow building basic forms (#44214) * Initial forms POC * this was just copy-pasted :facepalm: * Use RichText for labels * Make things editable * Add initial PHP implementation & filters for form action & method * Add name attribute in fields * rename action * Default form submissions to POST instead of GET * Add default action to send an email on form-submission * Move "name" attribute to the Advanced section. * Add checkbox field-type * update block.json * Add a class on labels * Add test stylesheet * fix input styles loading * basic styles * split input-types array to a separate utils file * add ALLOWED_BLOCKS in forms * Add input fields variations * fix admin styles for inputs * refactor inline labels & other fixes * Allow marking fields as required * Allow defining a form-ID * generate fixtures * typo * forgot to rename these 2 * Add a template * Add block supports * Styling tweak * Mark as an experiment * fixtures * Rename input-field block to form-input * Add default init.js file for the form-input block * Use WP_HTML_Tag_Processor * use the HTML for the label * Use an attribute for "required" * fix for submit button label * Add placeholder to inputs * Revert "Add placeholder to inputs" This reverts commit c0e6e63d8371f5c6340577c3135eb7993fc1992e. * Remove identifier * cleanup * re-introduce placeholders * Use a div instead of label in the editor to avoid on-click focus issues * Simplification for textareas * Use the big appender when there are no inner blocks * Add form-submit-button block * implement form-submit button & improve variations * Add support for colors and border-radius to input blocks * make input fields select the block (wrong position for blockProps) * add __experimentalRole: content * fix HTML escaping in labels * CSS fixes * Use a nonce to improve security * Check that the nonce exists before checking its validity. * Use a new REST API endpoint for form submissions * Add support for link-colors * Don't include rest_route in the email to be sent * Add icons * fix alignment of checkboxes * Remove color options from fields * Add margins support to inputs + default margin-bottom * Fix the name attribute generated from labels * Move form-submissions class to the experimental folder * Add a filter for wp_kses_allowed_html * Move rest-api registration to 6.3 * change since to 6.3.0 * Fix method comment * Remove restriction (1 per page) in form-submit-button * Add a bottom margin so that the appender can appear * Remove "Website" from default template * update the docs * Update packages/block-library/src/form-input/style.scss Co-authored-by: Carolina Nymark * Move to experimental * Remove default values from block.json * Tweak for comment-forms * fix rebase conflict * Use the sitename * Fix URL * Add advanced options to the form * Remove REST-API endpoint & use the advanced fields * Add back empty line * Only add `action` if not empty * Handle contact forms * Change help text to clarify what happens when value is empty * fix doc * Add comment-form variation * Remove icon from comment-form variation * Handle GET data in contact forms as well * Redirect to same page on contact-form submission * Increase padding-bottom to 0.5em * Remove formId attribute. We already have an anchor * labels cannot include div elements. Use a span instead. * textarea elements should not have a type defined. * manually update fixtures * Move inspector controls from advanced section to normal * Add visibilityPermissions attribute * Rename comment-form variation * Add option to send email, and a field to define the address * Filter the email content to allow 3rd-party plugins to change things * Basic sanitization for the email content * Code improvement * Add render_block_core_form_email_sent action * It's possible to use comma-separated values for the email address * Add a `hidden` input-type * remove columns and groups for now There's an issue and we can't add input fields inside columns and/or groups etc. Inputs can only be added as a direct descendant of the form block. * Checkbox labels should always be inline * Default inlineLabel to true for checkboxes * coding fix * Add ADMIN_URL placeholder (facilitates ajax requests) * Add a privacy-request form * Make methods hookable * Add form-submission-notification block * add it to the list of allowed blocks * add form notifications to variations * improve default notifications * Now that we have variations we don't need the separate control * Default to success notification * Add editor styles for notifications * condition is not necessary * Remove redirect for emails forms. Still buggy, need to figure out smthng * Refactor emails form & add a view script * Don't show `hidden` inputs in the UI * rename functions * Update styles for notifications in the editor * remove hidden variation * Remove formOptions & fix controls spacing * fix styles for inline labels * CS fix * Check if the experiment is enabled before registering the blocks * Only localize the script if the experiment is enabled * Only filter wp_kses_allowed_html if the experiment is active * fix filter return * Add experiment to the tests --------- Co-authored-by: Carolina Nymark --- docs/reference-guides/core-blocks.md | 39 ++++ lib/blocks.php | 5 + lib/experimental/editor-settings.php | 12 + lib/experimental/kses-allowed-html.php | 43 ++++ lib/experiments-page.php | 11 + lib/load.php | 2 + packages/block-library/src/editor.scss | 2 + .../block-library/src/form-input/block.json | 72 ++++++ packages/block-library/src/form-input/edit.js | 151 ++++++++++++ .../block-library/src/form-input/editor.scss | 24 ++ .../block-library/src/form-input/index.js | 20 ++ .../block-library/src/form-input/index.php | 45 ++++ packages/block-library/src/form-input/init.js | 6 + packages/block-library/src/form-input/save.js | 83 +++++++ .../block-library/src/form-input/style.scss | 61 +++++ .../src/form-input/variations.js | 82 +++++++ .../form-submission-notification/block.json | 18 ++ .../src/form-submission-notification/edit.js | 63 +++++ .../form-submission-notification/editor.scss | 45 ++++ .../src/form-submission-notification/index.js | 26 +++ .../form-submission-notification/index.php | 48 ++++ .../src/form-submission-notification/init.js | 6 + .../src/form-submission-notification/save.js | 28 +++ .../variations.js | 59 +++++ .../src/form-submit-button/block.json | 13 ++ .../src/form-submit-button/edit.js | 33 +++ .../src/form-submit-button/index.js | 18 ++ .../src/form-submit-button/init.js | 6 + .../src/form-submit-button/save.js | 14 ++ .../src/form-submit-button/style.scss | 3 + packages/block-library/src/form/block.json | 59 +++++ packages/block-library/src/form/edit.js | 179 ++++++++++++++ packages/block-library/src/form/index.js | 20 ++ packages/block-library/src/form/index.php | 218 ++++++++++++++++++ packages/block-library/src/form/init.js | 6 + packages/block-library/src/form/save.js | 20 ++ packages/block-library/src/form/utils.js | 39 ++++ packages/block-library/src/form/variations.js | 139 +++++++++++ packages/block-library/src/form/view.js | 41 ++++ packages/block-library/src/index.js | 10 + packages/block-library/src/style.scss | 1 + phpunit/bootstrap.php | 1 + .../fixtures/blocks/core__form-input.html | 3 + .../fixtures/blocks/core__form-input.json | 12 + .../blocks/core__form-input.parsed.json | 14 ++ .../blocks/core__form-input.serialized.html | 3 + .../fixtures/blocks/core__form.html | 21 ++ .../fixtures/blocks/core__form.json | 63 +++++ .../fixtures/blocks/core__form.parsed.json | 84 +++++++ .../blocks/core__form.serialized.html | 19 ++ 50 files changed, 1990 insertions(+) create mode 100644 lib/experimental/kses-allowed-html.php create mode 100644 packages/block-library/src/form-input/block.json create mode 100644 packages/block-library/src/form-input/edit.js create mode 100644 packages/block-library/src/form-input/editor.scss create mode 100644 packages/block-library/src/form-input/index.js create mode 100644 packages/block-library/src/form-input/index.php create mode 100644 packages/block-library/src/form-input/init.js create mode 100644 packages/block-library/src/form-input/save.js create mode 100644 packages/block-library/src/form-input/style.scss create mode 100644 packages/block-library/src/form-input/variations.js create mode 100644 packages/block-library/src/form-submission-notification/block.json create mode 100644 packages/block-library/src/form-submission-notification/edit.js create mode 100644 packages/block-library/src/form-submission-notification/editor.scss create mode 100644 packages/block-library/src/form-submission-notification/index.js create mode 100644 packages/block-library/src/form-submission-notification/index.php create mode 100644 packages/block-library/src/form-submission-notification/init.js create mode 100644 packages/block-library/src/form-submission-notification/save.js create mode 100644 packages/block-library/src/form-submission-notification/variations.js create mode 100644 packages/block-library/src/form-submit-button/block.json create mode 100644 packages/block-library/src/form-submit-button/edit.js create mode 100644 packages/block-library/src/form-submit-button/index.js create mode 100644 packages/block-library/src/form-submit-button/init.js create mode 100644 packages/block-library/src/form-submit-button/save.js create mode 100644 packages/block-library/src/form-submit-button/style.scss create mode 100644 packages/block-library/src/form/block.json create mode 100644 packages/block-library/src/form/edit.js create mode 100644 packages/block-library/src/form/index.js create mode 100644 packages/block-library/src/form/index.php create mode 100644 packages/block-library/src/form/init.js create mode 100644 packages/block-library/src/form/save.js create mode 100644 packages/block-library/src/form/utils.js create mode 100644 packages/block-library/src/form/variations.js create mode 100644 packages/block-library/src/form/view.js create mode 100644 test/integration/fixtures/blocks/core__form-input.html create mode 100644 test/integration/fixtures/blocks/core__form-input.json create mode 100644 test/integration/fixtures/blocks/core__form-input.parsed.json create mode 100644 test/integration/fixtures/blocks/core__form-input.serialized.html create mode 100644 test/integration/fixtures/blocks/core__form.html create mode 100644 test/integration/fixtures/blocks/core__form.json create mode 100644 test/integration/fixtures/blocks/core__form.parsed.json create mode 100644 test/integration/fixtures/blocks/core__form.serialized.html diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index a7570bf590a39..8e38632113868 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -277,6 +277,45 @@ Display footnotes added to the page. ([Source](https://github.com/WordPress/gute - **Supports:** color (background, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~, ~~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 Use the classic WordPress editor. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/freeform)) diff --git a/lib/blocks.php b/lib/blocks.php index 537fa9ce4b45e..1794762b010db 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 f5cc2ec969d33..2c7d6310005bf 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 0000000000000..122faef7b4ca2 --- /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 3bf53efd61622..0bcd28b2aa2c4 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 d87c53081903e..a3a61407764b5 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-library/src/editor.scss b/packages/block-library/src/editor.scss index 07c58599c5098..5f3d962ae7afa 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/form-input/block.json b/packages/block-library/src/form-input/block.json new file mode 100644 index 0000000000000..dbe182f03b499 --- /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 0000000000000..0742c22c22f42 --- /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 } + + setAttributes( { value: event.target.value } ) + } + /> + + ); + } + + return ( +
+ { 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 0000000000000..2ac67e6615ed4 --- /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 0000000000000..b700e0ade6ca7 --- /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 0000000000000..f905c2bc6e19f --- /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 0000000000000..79f0492c2cb2f --- /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 0000000000000..0cca31ca423ee --- /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 0000000000000..d45fc8d7f1f72 --- /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 0000000000000..cc205feb89501 --- /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 0000000000000..62284d35ab4dd --- /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 0000000000000..4425a4d9147df --- /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 0000000000000..a8d3f4e3d9263 --- /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 0000000000000..67c359374eec1 --- /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 0000000000000..0a57866f37edf --- /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 0000000000000..a7f22ef02d640 --- /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 0000000000000..7b3c6c895c192 --- /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 0000000000000..b154a26e5e6a4 --- /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 0000000000000..faa938e9bbc24 --- /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 0000000000000..f8d7a65c6877a --- /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 0000000000000..4c60b5f5c2063 --- /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 0000000000000..79f0492c2cb2f --- /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 0000000000000..ba361ebe9db20 --- /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 0000000000000..400016b1618d4 --- /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 0000000000000..951d1dce4224e --- /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 0000000000000..d8ae9ea5e7553 --- /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 0000000000000..b700e0ade6ca7 --- /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 0000000000000..0dbbaf6838740 --- /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 0000000000000..79f0492c2cb2f --- /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 0000000000000..a824fc076d2ac --- /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 0000000000000..e541f34bbc887 --- /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 0000000000000..da3fcbbf03942 --- /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 0000000000000..05efe95da545c --- /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/index.js b/packages/block-library/src/index.js index 736b552bf4259..e2e0fd9e414ef 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/style.scss b/packages/block-library/src/style.scss index c4b0a37e6354d..790e09535f4b6 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/phpunit/bootstrap.php b/phpunit/bootstrap.php index 7084df68443ba..acc7cfde89dbd 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/test/integration/fixtures/blocks/core__form-input.html b/test/integration/fixtures/blocks/core__form-input.html new file mode 100644 index 0000000000000..718c592641bc3 --- /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 0000000000000..33802bbcc2088 --- /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 0000000000000..73058fc2e17f0 --- /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 0000000000000..4e1f6b77998de --- /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 0000000000000..ab18e0e11c81a --- /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 0000000000000..ba07b17e4d00c --- /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 0000000000000..379bee84c84e1 --- /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 0000000000000..58a2a49967eb5 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form.serialized.html @@ -0,0 +1,19 @@ + +
+ + + + + + + + + + + + + +
+ +
+ From af6cb47279f8654cbae4f8dcb6c28858b0422239 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 5 Oct 2023 12:32:01 +0400 Subject: [PATCH 03/20] Site Editor: Do not display 'trashed' navigation menus in Sidebar (#55072) * Site Editor: Do not display 'trashed' navigation menus in Sidebar * Extract selector into a hook Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> --------- Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> --- ...template-part-navigation-menu-list-item.js | 9 ++---- .../template-part-navigation-menu.js | 10 ++---- .../use-navigation-menu-title.js | 32 +++++++++++++++++++ 3 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-title.js diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js index 8a493130ad27d..22d9d841dc6fd 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js @@ -1,23 +1,18 @@ /** * WordPress dependencies */ -import { useEntityProp } from '@wordpress/core-data'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import SidebarNavigationItem from '../sidebar-navigation-item'; +import useNavigationMenuTitle from './use-navigation-menu-title'; import { useLink } from '../routes/link'; import { NAVIGATION_POST_TYPE } from '../../utils/constants'; export default function TemplatePartNavigationMenuListItem( { id } ) { - const [ title ] = useEntityProp( - 'postType', - NAVIGATION_POST_TYPE, - 'title', - id - ); + const title = useNavigationMenuTitle( id ); const linkInfo = useLink( { postId: id, diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js index 40012ec46a85e..b7f9b5295fd0f 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js @@ -3,21 +3,15 @@ */ import { __ } from '@wordpress/i18n'; import { __experimentalHeading as Heading } from '@wordpress/components'; -import { useEntityProp } from '@wordpress/core-data'; /** * Internal dependencies */ import NavigationMenuEditor from '../sidebar-navigation-screen-navigation-menu/navigation-menu-editor'; -import { NAVIGATION_POST_TYPE } from '../../utils/constants'; +import useNavigationMenuTitle from './use-navigation-menu-title'; export default function TemplatePartNavigationMenu( { id } ) { - const [ title ] = useEntityProp( - 'postType', - NAVIGATION_POST_TYPE, - 'title', - id - ); + const title = useNavigationMenuTitle( id ); if ( ! id || title === undefined ) { return null; diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-title.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-title.js new file mode 100644 index 0000000000000..4585c98ce3e1f --- /dev/null +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-title.js @@ -0,0 +1,32 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { NAVIGATION_POST_TYPE } from '../../utils/constants'; + +export default function useNavigationMenuTitle( id ) { + return useSelect( + ( select ) => { + 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 ] + ); +} From 9ef96c5b29f5c3dab8da9acb736e85c2a6f6952b Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 5 Oct 2023 09:37:15 +0100 Subject: [PATCH 04/20] Image: Fix Lightbox display bug in Classic Themes. (#54837) * If current theme is not a block theme add a default value for $background_color and $close_button_color. * Set lightbox buttons' background & border to none on hover & focus * Change logic to support lightbox in classic themes * Update logic to avoid unnecessary calls * Add style fixes * Remove unnecessary code * Fix close button color --------- Co-authored-by: Mario Santos Co-authored-by: Ricardo Artemio Morales --- packages/block-library/src/image/index.php | 16 ++++++++++++++-- packages/block-library/src/image/style.scss | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 54aa8fca6d5f4..cba0203b477a4 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 5c3552fd80c2e..752ff773394a4 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 { From c0cacda2793e15d001b73a31f5ca1ee0148ba806 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 5 Oct 2023 13:57:52 +0400 Subject: [PATCH 05/20] Migrate demo page tests to Playwright (#55054) * Migrate demo page tests to Playwright * Remove old test file * make check more strict Co-authored-by: Bart Kalisz --- packages/e2e-tests/specs/local/demo.test.js | 50 --------------------- test/e2e/specs/editor/local/demo.spec.js | 38 ++++++++++++++++ 2 files changed, 38 insertions(+), 50 deletions(-) delete mode 100644 packages/e2e-tests/specs/local/demo.test.js create mode 100644 test/e2e/specs/editor/local/demo.spec.js 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 f3a1433d72088..0000000000000 --- 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/test/e2e/specs/editor/local/demo.spec.js b/test/e2e/specs/editor/local/demo.spec.js new file mode 100644 index 0000000000000..acfee9296e232 --- /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(); + } ); +} ); From 6fe6d2b1f73b0f9876396f02390c43019348a104 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Thu, 5 Oct 2023 10:50:41 +0000 Subject: [PATCH 06/20] E2E Utils: Use frameLocator for retrieving editor canvas (#54911) --- .../src/admin/create-new-post.js | 20 +-- .../src/editor/get-blocks.ts | 4 + .../src/editor/get-edited-post-content.ts | 2 + .../src/editor/index.ts | 15 ++- .../src/editor/insert-block.ts | 4 + .../src/editor/set-content.ts | 4 + .../src/editor/set-is-fixed-toolbar.ts | 2 + .../src/editor/switch-to-legacy-canvas.ts | 20 +++ .../src/editor/transform-block-to.ts | 4 + .../src/page-utils/drag-files.ts | 46 +++---- test/e2e/specs/editor/blocks/buttons.spec.js | 4 +- test/e2e/specs/editor/blocks/classic.spec.js | 16 +-- test/e2e/specs/editor/blocks/code.spec.js | 4 +- test/e2e/specs/editor/blocks/comments.spec.js | 6 +- test/e2e/specs/editor/blocks/gallery.spec.js | 8 +- test/e2e/specs/editor/blocks/group.spec.js | 30 +++-- test/e2e/specs/editor/blocks/heading.spec.js | 36 +++-- test/e2e/specs/editor/blocks/html.spec.js | 9 +- test/e2e/specs/editor/blocks/image.spec.js | 27 ++-- test/e2e/specs/editor/blocks/list.spec.js | 116 ++++++++++++---- .../editor/blocks/navigation-colors.spec.js | 8 +- .../navigation-frontend-interactivity.spec.js | 8 +- .../e2e/specs/editor/blocks/paragraph.spec.js | 17 +-- .../e2e/specs/editor/blocks/pullquote.spec.js | 4 +- test/e2e/specs/editor/blocks/quote.spec.js | 26 ++-- .../e2e/specs/editor/blocks/separator.spec.js | 4 +- test/e2e/specs/editor/blocks/spacer.spec.js | 8 +- test/e2e/specs/editor/blocks/table.spec.js | 94 ++++++++----- .../editor/plugins/block-variations.spec.js | 24 +++- .../editor/plugins/custom-post-types.spec.js | 12 +- .../specs/editor/plugins/format-api.spec.js | 4 +- .../specs/editor/plugins/hooks-api.spec.js | 8 +- .../inner-blocks-allowed-blocks.spec.js | 4 +- test/e2e/specs/editor/plugins/nonce.spec.js | 7 +- .../plugins/post-type-templates.spec.js | 22 +-- .../editor/plugins/wp-editor-meta-box.spec.js | 2 +- .../various/a11y-region-navigation.spec.js | 8 ++ test/e2e/specs/editor/various/a11y.spec.js | 19 +-- .../various/adding-inline-tokens.spec.js | 4 +- .../various/autocomplete-and-mentions.spec.js | 88 ++++++------ .../block-hierarchy-navigation.spec.js | 28 ++-- .../editor/various/block-locking.spec.js | 16 ++- .../specs/editor/various/block-mover.spec.js | 4 +- .../editor/various/block-moving-mode.spec.js | 24 ++-- .../compatibility-classic-editor.spec.js | 2 +- .../editor/various/content-only-lock.spec.js | 10 +- .../editor/various/copy-cut-paste.spec.js | 114 ++++++++++------ .../editor/various/draggable-blocks.spec.js | 35 ++--- .../editor/various/font-size-picker.spec.js | 48 +++---- .../specs/editor/various/footnotes.spec.js | 53 +++++--- .../various/inner-blocks-templates.spec.js | 8 +- .../editor/various/inserting-blocks.spec.js | 30 ++--- .../keep-styles-on-block-transforms.spec.js | 19 ++- .../specs/editor/various/list-view.spec.js | 7 - .../e2e/specs/editor/various/mentions.spec.js | 12 +- .../various/multi-block-selection.spec.js | 121 ++++++++--------- .../e2e/specs/editor/various/new-post.spec.js | 7 +- .../various/post-editor-template-mode.spec.js | 1 - .../editor/various/post-visibility.spec.js | 4 +- test/e2e/specs/editor/various/preview.spec.js | 62 +++++---- .../specs/editor/various/rich-text.spec.js | 125 +++++++++++++----- test/e2e/specs/editor/various/rtl.spec.js | 4 +- .../editor/various/splitting-merging.spec.js | 5 +- .../various/toolbar-roving-tabindex.spec.js | 4 +- test/e2e/specs/editor/various/undo.spec.js | 46 +++++-- .../specs/editor/various/writing-flow.spec.js | 59 +++++---- .../interactivity/directive-init.spec.ts | 4 +- .../interactivity/directive-slots.spec.ts | 2 +- .../specs/site-editor/block-removal.spec.js | 2 +- .../specs/site-editor/command-center.spec.js | 5 +- .../specs/site-editor/font-library.spec.js | 4 +- .../site-editor/global-styles-sidebar.spec.js | 2 +- .../specs/site-editor/hybrid-theme.spec.js | 2 +- .../site-editor/iframe-rendering.spec.js | 9 +- test/e2e/specs/site-editor/list-view.spec.js | 6 +- .../site-editor/multi-entity-saving.spec.js | 2 +- .../site-editor/push-to-global-styles.spec.js | 2 +- .../site-editor/site-editor-inserter.spec.js | 2 +- test/e2e/specs/site-editor/style-book.spec.js | 14 +- .../site-editor/style-variations.spec.js | 17 +-- .../specs/site-editor/template-part.spec.js | 42 +++--- .../specs/site-editor/template-revert.spec.js | 6 +- .../user-global-styles-revisions.spec.js | 6 +- .../specs/site-editor/writing-flow.spec.js | 4 +- .../specs/widgets/customizing-widgets.spec.js | 20 ++- 85 files changed, 1037 insertions(+), 710 deletions(-) create mode 100644 packages/e2e-test-utils-playwright/src/editor/switch-to-legacy-canvas.ts 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 ef077e8e935e3..81822e2514a73 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 f5adb295a00a5..858c9da980fc1 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 c294cd7d1b939..a0f14d70a2e83 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 395fbb4f98b69..673149d4e69e0 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 646bcd1b410a1..ef923cf667d37 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 ff64a81db4d6c..d44d22f74d049 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 0e642a1de7662..93c2cd14e1ebc 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 0000000000000..b7c4c84487dbb --- /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 9701eb70ec65f..75102983069d4 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 f6378b73e33ec..fa43fc76d27c3 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/test/e2e/specs/editor/blocks/buttons.spec.js b/test/e2e/specs/editor/blocks/buttons.spec.js index 04cd358729a11..dcddfca2b5b28 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 30f6051fab29b..2dcc526851743 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 c4037d50b7dd5..6abfb15d10b83 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 f950036539c11..ef693cb8b5bb9 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 2de22245eb5b0..ccf522d8c4d53 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 4af974d7b4af6..705bce2c3f2c9 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 99a875f081018..f034da6efe617 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 b538e7598d96b..db3ff72e3ab6e 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 f4396982bb997..6716a8fb5eac4 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 42ecb29aa6650..1ddd4af8ab2e1 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 1c745d7a0e57b..7e761f1861856 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 fcc15890a8249..3cf3654870a35 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 f2a6698f5065f..33f833ca53678 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 13b7ee341ede7..d25dedd4a0a39 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 a2e088e14c398..70c61535e71bf 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 77e978a0df302..f089402514623 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 689989f9022a3..1e6dfdcd76e18 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/plugins/block-variations.spec.js b/test/e2e/specs/editor/plugins/block-variations.spec.js index 0b445aee451c6..9f12c987efd39 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 1cceddbf04ad1..17a497f26cee0 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 f98d8292ea8f6..1e631615313bd 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 b3b5ed68f66f1..95bc5bf8bfd2c 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 cd6f440d2db0e..c0627121f1649 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 a05f3618b3641..0ffe0c5efb813 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 c743d08d8ae68..9b2abddb0dd0e 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 c5eafdafe918d..710e06b35e124 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 2c28ba311d8c7..d8dda0cabdf84 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 8b819d3866b6c..0a5e421debedb 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 0facffd9097e8..15f9d9ea87732 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 3cfc838fcadf2..6e1e0edfa3aba 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,7 +427,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( '@fr' ); await expect( page.locator( 'role=option', { hasText: 'Frodo Baggins' } ) @@ -444,7 +450,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( '@fr' ); await expect( page.locator( 'role=option', { hasText: 'Frodo Baggins' } ) @@ -460,7 +468,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 +480,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 +492,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 4dc49bc0f7510..f0bfe5bff203f 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 67a7f0357bc2a..b40e7a4b7448a 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 4ed90191f2558..5c3b04a069774 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 59c5368285429..5b8ef6bdcd051 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 a1efbfa579b9a..71522c1d439a5 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 03282357a72b6..e7d52562636f3 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 823926c1121a0..4c24934924366 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 2d95e3bdefe97..fb56b43dc6e03 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 ddc47e3ee6de6..5c6cb4b186e25 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 e3aa17c5a101c..14a2fc653e387 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 47ef0dfe74791..0f9aed33d0773 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 cfef686ef5c87..a48fe117c97a2 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 8d1f37187fee1..57b958fdfc4b4 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 7fd21085deae2..222d743acdf39 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 061b8d67a0801..fef3b1c3e3d2e 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 3374a13b98eb7..2473be99d218c 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,18 +1188,18 @@ test.describe( 'Multi-block selection', () => { attributes: { content: ']2' }, } ); // Focus and move the caret to the end. - await editor.canvas + await page .getByRole( 'document', { name: 'Block: Paragraph' } ) .filter( { hasText: ']2' } ) .click(); 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 + await page .getByRole( 'document', { name: 'Block: Paragraph' } ) .filter( { hasText: '1[' } ) .click( { @@ -1354,9 +1343,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 4b192693c07b0..9d6f4ef45d9db 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 5d63ff789e4ec..629a437a41665 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 3f83221c27b81..365209ef2e4e5 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 a3fa4544f39f8..cfec384adba9b 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 4ebd2cd450e70..2969a33d25485 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 8475605e339fc..aaf1186cc5aba 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 1c5e12be8abb1..29e7e5d64522c 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 23186dfff2de9..e706dfc3607dc 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 dfdbef60ffaa9..51683997aaf6f 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 81bbcbbc758b9..98bb00a596f03 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 aa81ab1ea61db..e18341a48fa3f 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 d93e50f767215..195af52fdb1bd 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 c1d6cdfea0022..5c46ac769efd3 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 9d22248bc2362..60c5ec30b1247 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 e6521cb7c540b..6aca027a30e78 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 02717f1b19432..f255640cb2f12 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 060daf508491a..7c9f3fc3a5e23 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 02389cc936f06..4391f134a9f80 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 3057feaf7772b..ed64574168bd0 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 b4d2f1dbda0d0..bb062aafa3e7f 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 937b2aea0fb45..9507245c192d2 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 f8ab0a534d858..98cb8e4e74149 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 eeaa1270f3ff9..08f5e6463ebc7 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 ee71e856269b0..8868c73300668 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 cd81a616b1fee..d1c215ec2a494 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 c8edc034901a2..c281b71d16a18 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 a7ecebfd9ed44..61fbf7c795a60 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 f9681a5ea2d46..8ee6ce0e56557 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 e771c92fc24ee..6a6a51cae2686 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' ); From 5735a49a1cf5647125872b658464d949e50b4290 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Thu, 5 Oct 2023 12:26:57 +0100 Subject: [PATCH 07/20] Update LinkControl docs with advice to memoize value prop (#54659) * Update docs * Update reasoning * Update wording to match review comment * Actually update the text --- .../src/components/link-control/README.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/block-editor/src/components/link-control/README.md b/packages/block-editor/src/components/link-control/README.md index fef68318867e8..9479863e36f40 100644 --- a/packages/block-editor/src/components/link-control/README.md +++ b/packages/block-editor/src/components/link-control/README.md @@ -59,6 +59,29 @@ The resulting default properties of `value` include: - `title` (`string`, optional): Link title. - `opensInNewTab` (`boolean`, optional): Whether link should open in a new browser tab. This value is only assigned when not providing a custom `settings` prop. +Note: `` 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` From 94de5dbbc1e896db6988a6a6e3e256492cea62ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:50:14 +0200 Subject: [PATCH 08/20] Add `status` entity and use it in "admin views" experiment (#55050) --- packages/core-data/src/entities.js | 9 +++++++ .../src/components/page-pages/index.js | 24 +++++++++++-------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index fde8cdba93717..3fb3af96eb9ad 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/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index e7b8f913efe7c..b767ad77adc6d 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() { Date: Thu, 5 Oct 2023 09:56:58 -0400 Subject: [PATCH 09/20] feat: Split formatted text on triple Enter (#53354) * feat: Split formatted text on triple enter It is difficult to exit a formatted text block, e.g. Verse, using only the keyboard. This splits the block when pressing enter and the two previous characters are line breaks. * feat: Split Code block on triple enter Apply Verse and Preformatted block behavior to the Code block. However, this applied code does not currently run as Code relies upon the older version of `PlainText`. * feat: Enable PlainText v2 for the Code block Improve splitting and merging block functionality by relying upon the more robust `RichText` component. Merging blocks into Code does not always work as expected, e.g. a Paragraph into a Code block fails. * fix: RichText applies direction-specific padding Certain contexts apply different padding for different directions, e.g. horizontal vs vertical, left vs right. * Revert "fix: RichText applies direction-specific padding" This reverts commit 62056514982bc98b59024f7c46750f8b8c4866ea. * Revert "feat: Enable PlainText v2 for the Code block" This reverts commit 37d3fa3c493b06425da20917e899e55ec20d9eb0. * test: Split formatted text block on triple Enter * docs: Add change log entry --- .../src/components/rich-text/index.native.js | 22 +++++++++-- .../block-library/src/code/edit.native.js | 16 +++++++- .../src/preformatted/test/edit.native.js | 38 +++++++++++++++++++ .../src/verse/test/edit.native.js | 37 ++++++++++++++++++ packages/react-native-editor/CHANGELOG.md | 1 + 5 files changed, 110 insertions(+), 4 deletions(-) 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 8c1afdd8903bd..32bd1afd3d540 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 58188d0b2119f..3353dbc3c25a0 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() ) ) + } /> </View> ); diff --git a/packages/block-library/src/preformatted/test/edit.native.js b/packages/block-library/src/preformatted/test/edit.native.js index 1fdb4532dacab..153b1b9e9b0ab 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', () => { <!-- /wp: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( ` + "<!-- wp:preformatted --> + <pre class="wp-block-preformatted">Hello</pre> + <!-- /wp:preformatted --> + + <!-- wp:paragraph --> + <p></p> + <!-- /wp:paragraph -->" + ` ); + } ); } ); diff --git a/packages/block-library/src/verse/test/edit.native.js b/packages/block-library/src/verse/test/edit.native.js index c91c261c89c35..8794419775d32 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', () => { <!-- /wp:verse -->" ` ); } ); + + 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( ` + "<!-- wp:verse --> + <pre class="wp-block-verse">Hello</pre> + <!-- /wp:verse --> + + <!-- wp:paragraph --> + <p></p> + <!-- /wp:paragraph -->" + ` ); + } ); } ); diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 9316c4693cfaa..600f855ba0ec1 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] From b8b89cecd6b6e274d70867d9d0dd7acd005be3e5 Mon Sep 17 00:00:00 2001 From: Bart Kalisz <bartlomiej.kalisz@gmail.com> Date: Thu, 5 Oct 2023 14:30:38 +0000 Subject: [PATCH 10/20] E2E: Try to fix the flaky autocomplete spec (#55081) --- .../various/autocomplete-and-mentions.spec.js | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) 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 6e1e0edfa3aba..ec0ca999993c2 100644 --- a/test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js +++ b/test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js @@ -427,23 +427,43 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => { page, editor, } ) => { + const typingDelay = 100; + await editor.canvas - .locator( 'role=button[name="Add default block"i]' ) + .getByRole( 'button', { name: 'Add default block' } ) .click(); - await page.keyboard.type( '@fr' ); + + 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( `<!-- wp:paragraph --> -<p>@ringbearer +thebetterhobbit</p> -<!-- /wp:paragraph -->` ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '@ringbearer +thebetterhobbit' }, + }, + ] ); } ); test( 'should hide UI when selection changes (by keyboard)', async ( { From ae5761b0b44ee86974e1ae87f65e46551ff50e84 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Thu, 5 Oct 2023 17:19:45 +0100 Subject: [PATCH 11/20] Documentation: Explain how to translate Gutenberg in standalone apps using the editor packages (#55080) --- .../basic-concepts/internationalization.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/platform-docs/docs/basic-concepts/internationalization.md b/platform-docs/docs/basic-concepts/internationalization.md index c15a1e2e68145..654d70070f1cb 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 +<html dir="rtl"> + <!-- rest of your app --> +</html> +``` From fcb107978ef8b06ef1996176c46fada3e0b38eec Mon Sep 17 00:00:00 2001 From: David Calhoun <github@davidcalhoun.me> Date: Thu, 5 Oct 2023 13:02:24 -0400 Subject: [PATCH 12/20] build: Support custom mobile Demo editor setup configuration (#54957) * build: Support custom mobile Demo editor setup configuration Applying optional setup configuration files allows developers to modify the Demo editor environment via Hooks, e.g. to set custom `initialHtml` for the editor. * test: Disable local Demo editor configuration for tests The `require.context` function is undefined in the test environment, as it is a Metro-specific capability. The most straightforward way to avoid test failures is to disable this code that relates to the local development workflow. * docs: Document Demo editor customization Contributors will likely never discover or benefit from this customization ability if it is not documented. --- .gitignore | 1 + .../getting-started-react-native.md | 34 +++++++++++++++++++ packages/react-native-editor/metro.config.js | 1 + packages/react-native-editor/src/index.js | 6 ++++ 4 files changed, 42 insertions(+) diff --git a/.gitignore b/.gitignore index b5cd0124e5d4b..48cd7580beed1 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 e13ac748aaf29..53d5f7eee5a12 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. + +<details><summary>Example setup-local.js</summary> + +```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 = ` +<!-- wp:heading --> +<h2 class="wp-block-heading">Just a Heading</h2> +<!-- /wp:heading --> +`; +``` + +</details> + ### 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/packages/react-native-editor/metro.config.js b/packages/react-native-editor/metro.config.js index 05e57e3cfbcab..307853a612c8c 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 a4ca31e3e9488..32ba1c6f3441d 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 ); From 7ffd96d850a7da4bb9327d69c25b2b54c20fa99a Mon Sep 17 00:00:00 2001 From: Rich Tabor <hi@richtabor.com> Date: Thu, 5 Oct 2023 13:29:35 -0400 Subject: [PATCH 13/20] Set inserter to false for footnotes block (#55058) --- docs/reference-guides/core-blocks.md | 2 +- packages/block-library/src/footnotes/block.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 8e38632113868..ffd45a7cb8c75 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -274,7 +274,7 @@ 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 diff --git a/packages/block-library/src/footnotes/block.json b/packages/block-library/src/footnotes/block.json index 4a2db992863db..3192df7796978 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, From bd9a1cf0f61bf804957fdfa074b790e59acafccb Mon Sep 17 00:00:00 2001 From: Hiroshi Urabe <mail@torounit.com> Date: Fri, 6 Oct 2023 04:02:15 +0900 Subject: [PATCH 14/20] Fix incorrect resource URL in source map for @wordpress packages. (#51401) * Change the URL of the packages output in the source map. --------- Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --- tools/webpack/packages.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js index 3f105886cd2d1..a76889622b4a2 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, From 0816b4cdf016f64f580853dbd39c9228eec5c5f4 Mon Sep 17 00:00:00 2001 From: Mitchell Austin <mr.fye@oneandthesame.net> Date: Thu, 5 Oct 2023 13:40:52 -0700 Subject: [PATCH 15/20] `Modal`: fix closing when contained iframe is focused (#51602) * Fix modal closing when a contained iframe is focused Remove focus outside hook. * Test: add iframe to Modal component * Monopolize open modal * Revise comment * Remove needless condition * Avoid running effect due to unstable prop * Permit nested Modals * Robustly support nested Modals * Preserve current behavior for nested modals * Split effect to separate concerns * Add unit test for request closing of nested modals * Call onRequestClose for nested modal when outer modal unmounts * Comment and rename per feedback * Add changelog entry * Revert "Test: add iframe to Modal component" This reverts commit 6502514921a60e6e30cd5ec3e7cfb14d94b1ff0a. * Fix changelog --------- Co-authored-by: Tetsuaki Hamano <tetsuaki.hamano@gmail.com> --- packages/components/CHANGELOG.md | 4 + packages/components/src/modal/index.tsx | 80 ++++++++++++++------ packages/components/src/modal/test/index.tsx | 29 +++++++ 3 files changed, 91 insertions(+), 22 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 779c1b4238290..e5f86ff78bc29 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 diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index 2746c40fcaab0..041c592166ab7 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 <div ref={ useMergeRefs( [ ref, forwardedRef ] ) } @@ -253,9 +286,6 @@ function UnforwardedModal( aria-labelledby={ contentLabel ? undefined : headingId } aria-describedby={ aria.describedby } tabIndex={ -1 } - { ...( shouldCloseOnClickOutside - ? focusOutsideProps - : {} ) } onKeyDown={ onKeyDown } > <div @@ -320,7 +350,13 @@ function UnforwardedModal( </div> </div> </StyleProvider> - </div>, + </div> + ); + + return createPortal( + <ModalContext.Provider value={ nestedDismissers.current }> + { modal } + </ModalContext.Provider>, document.body ); } diff --git a/packages/components/src/modal/test/index.tsx b/packages/components/src/modal/test/index.tsx index 69f28508c1405..9073735e94dbe 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 && ( + <Modal + onKeyDown={ ( { key } ) => { + if ( key === 'o' ) setIsShown( false ); + } } + onRequestClose={ noop } + > + <Modal onRequestClose={ onRequestClose }> + <p>Nested modal content</p> + </Modal> + </Modal> + ) } + </> + ); + }; + render( <RequestCloseOfNested /> ); + + await user.keyboard( 'o' ); + expect( onRequestClose ).toHaveBeenCalled(); + } ); + it( 'should accessibly hide and show siblings including outer modals', async () => { const user = userEvent.setup(); From 8ef6e404f1de2ca22e3921c8e454e4335c843143 Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Fri, 6 Oct 2023 09:43:35 +1100 Subject: [PATCH 16/20] Latest Posts: add screen reader title text to Read more links and use alternative to excerpt_more filter (#55029) * In the editor: adds a screen reader text span with the post title in the i18n interpolator In the frontend: removes excerpt_more filter so we don't override themes and also replaces the default ellipsis with an accessible read more link * Removing "of" preposition in favour of a semi-colon. "Read more" is already translated so using a specifier to add it to the string --- .../block-library/src/latest-posts/edit.js | 12 +++++++-- .../block-library/src/latest-posts/index.php | 26 +++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js index 7aaf1b3ecf0ed..586ecc5943273 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 … */ - __( ' … <a>Read more</a>' ), + sprintf( + /* translators: 1: The static string "Read more", 2: The post title only visible to screen readers. */ + __( '… <a>%1$s<span>: %2$s</span></a>' ), + __( 'Read more' ), + titleTrimmed || __( '(no title)' ) + ), { a: ( // eslint-disable-next-line jsx-a11y/anchor-has-content <a + className="wp-block-latest-posts__read-more" href={ post.link } rel="noopener noreferrer" onClick={ @@ -496,6 +501,9 @@ export default function LatestPostsEdit( { attributes, setAttributes } ) { } /> ), + span: ( + <span className="screen-reader-text" /> + ), } ) } </> diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index 356ba5032689f..d5f759c0c0e25 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( __( ' … <a href="%1$s" rel="noopener noreferrer">Read more</a>' ), 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. + * [&hellip;] is the default excerpt ending from wp_trim_excerpt() in Core. + */ + if ( str_ends_with( $trimmed_excerpt, ' [&hellip;]' ) ) { + $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. */ + __( '… <a href="%1$s" rel="noopener noreferrer">%2$s<span class="screen-reader-text">: %3$s</span></a>' ), + esc_url( $post_link ), + __( 'Read more' ), + esc_html( $title ) + ); + } + } + if ( post_password_required( $post ) ) { $trimmed_excerpt = __( 'This content is password protected.' ); } From ed29c89d55d0bff8e5c6ac5caa73770fdf625bb7 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:35:16 +1000 Subject: [PATCH 17/20] Elements: Add unit tests for class and style generation (#55113) Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com> --------- Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com> --- phpunit/block-supports/elements-test.php | 305 +++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 phpunit/block-supports/elements-test.php diff --git a/phpunit/block-supports/elements-test.php b/phpunit/block-supports/elements-test.php new file mode 100644 index 0000000000000..efea11887b620 --- /dev/null +++ b/phpunit/block-supports/elements-test.php @@ -0,0 +1,305 @@ +<?php + +/** + * Test the elements block support. + * + * @package Gutenberg + */ + +class WP_Block_Supports_Elements_Test extends WP_UnitTestCase { + /** + * @var string|null + */ + private $test_block_name; + + public function set_up() { + parent::set_up(); + $this->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' => '<p>Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>', + 'expected_markup' => '/^<p>Hello <a href="http:\/\/www.wordpress.org\/">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' => '<p>Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>', + 'expected_markup' => '/^<p>Hello <a href="http:\/\/www.wordpress.org\/">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' => '<p>Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>', + 'expected_markup' => '/^<p>Hello <a href="http:\/\/www.wordpress.org\/">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' => '<p>Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>', + 'expected_markup' => '/^<p class="wp-elements-[a-f0-9]{32}">Hello <a href="http:\/\/www.wordpress.org\/">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' => '<p>Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>', + 'expected_markup' => '/^<p class="wp-elements-[a-f0-9]{32}">Hello <a href="http:\/\/www.wordpress.org\/">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' => '<p>Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>', + 'expected_markup' => '/^<p class="wp-elements-[a-f0-9]{32}">Hello <a href="http:\/\/www.wordpress.org\/">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' => '<p class="has-dark-gray-background-color has-background">Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>', + 'expected_markup' => '/^<p class="has-dark-gray-background-color has-background wp-elements-[a-f0-9]{32}">Hello <a href="http:\/\/www.wordpress.org\/">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' => '<p id="anchor">Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>', + 'expected_markup' => '/^<p class="wp-elements-[a-f0-9]{32}" id="anchor">Hello <a href="http:\/\/www.wordpress.org\/">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 . '$/', + ), + ); + } +} From 05c93167e9f03c2019d1ed14c8b8381bec5078ba Mon Sep 17 00:00:00 2001 From: Kai Hao <kevin830726@gmail.com> Date: Fri, 6 Oct 2023 14:17:11 +0800 Subject: [PATCH 18/20] Try to fix multi-block-selection flakiness (#55075) * Try to fix multi-block-selection flakiness * Try clicking on strong text directly --- .../various/multi-block-selection.spec.js | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) 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 2473be99d218c..32fe45e6951bc 100644 --- a/test/e2e/specs/editor/various/multi-block-selection.spec.js +++ b/test/e2e/specs/editor/various/multi-block-selection.spec.js @@ -1188,10 +1188,17 @@ test.describe( 'Multi-block selection', () => { attributes: { content: ']2' }, } ); // Focus and move the caret to the end. - await page + 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 = page @@ -1199,14 +1206,11 @@ test.describe( 'Multi-block selection', () => { .getByText( '1', { exact: true } ); const strongBox = await strongText.boundingBox(); // Focus and move the caret to the end. - await page - .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. From 1d6e92b2403a24563562ef85676b6d01ebe6098b Mon Sep 17 00:00:00 2001 From: Andrew Hayward <andrew.hayward@automattic.com> Date: Fri, 6 Oct 2023 08:36:39 +0100 Subject: [PATCH 19/20] Migrate `Composite` component from `reakit` to `ariakit` (#54225) * Adding tests for `AlignmentMatrixControl` Making sure we have sufficient coverage to be confident that any changes don't break this component. * Adding `ariakit` Composite reference Temporarily writing it to v2 so other components continue to work while this is ongoing. * First pass at `ariakit` implementation * Moving change handler back to individual cells * Updating tests * Fixing test setup * Exporting `ariakit` composite in private API * Reworking `AlignmentMatrixControl` * Small test set-up refactor * Post-merge deprecation refactor * Refactoring `CircularOptionPicker` * Fixing broken tests * Updating CHANGELOG.md * Simplifying composite context usage * Refactoring and simplifying * Exporting `CompositeGroupLabel` * Renaming `asyncRender` to `renderAndInitCompositeStore` --- .../test/__snapshots__/control.js.snap | 3 +- packages/components/CHANGELOG.md | 1 + .../src/alignment-matrix-control/cell.tsx | 8 +- .../src/alignment-matrix-control/index.tsx | 85 ++++------- .../stories/index.story.tsx | 10 +- .../alignment-matrix-control/test/index.tsx | 135 +++++++++++++++--- .../src/alignment-matrix-control/utils.tsx | 42 ++++-- .../circular-option-picker-option.tsx | 62 ++++---- .../circular-option-picker.tsx | 39 ++--- .../src/circular-option-picker/types.ts | 11 +- packages/components/src/composite/v2.ts | 22 +++ packages/components/src/private-apis.ts | 12 ++ 12 files changed, 268 insertions(+), 162 deletions(-) create mode 100644 packages/components/src/composite/v2.ts 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 516fa70a321e9..242218705b3cf 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" /> <svg diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index e5f86ff78bc29..7103151110e2f 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -17,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 diff --git a/packages/components/src/alignment-matrix-control/cell.tsx b/packages/components/src/alignment-matrix-control/cell.tsx index 00daa6fb31695..162ca879f1a7e 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 ( <Tooltip text={ tooltipText }> - <CompositeItem as={ CellView } role="gridcell" { ...props }> + <CompositeItem + id={ id } + render={ <CellView { ...props } role="gridcell" /> } + > { /* 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 2d52bb0e24843..3de0e401187d5 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 ( <Composite - { ...props } - { ...composite } - aria-label={ label } - as={ Root } - className={ classes } - role="grid" - size={ width } + store={ compositeStore } + render={ + <Root + { ...props } + aria-label={ label } + className={ classes } + id={ baseId } + role="grid" + size={ width } + /> + } > { GRID.map( ( cells, index ) => ( - <CompositeGroup - { ...composite } - as={ Row } - role="row" - key={ index } - > + <CompositeRow render={ <Row role="row" /> } key={ index }> { cells.map( ( cell ) => { const cellId = getItemId( baseId, cell ); - const isActive = composite.currentId === cellId; + const isActive = cellId === activeId; return ( <Cell - { ...composite } id={ cellId } isActive={ isActive } key={ cell } value={ cell } - onFocus={ () => handleOnChange( cell ) } - tabIndex={ isActive ? 0 : -1 } /> ); } ) } - </CompositeGroup> + </CompositeRow> ) ) } </Composite> ); 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 24b496d1f2432..03bec9d92a8b7 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 ( <AlignmentMatrixControl { ...props } diff --git a/packages/components/src/alignment-matrix-control/test/index.tsx b/packages/components/src/alignment-matrix-control/test/index.tsx index a99f6d70135c5..6836bc7e45f95 100644 --- a/packages/components/src/alignment-matrix-control/test/index.tsx +++ b/packages/components/src/alignment-matrix-control/test/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render, screen, within } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; /** @@ -17,34 +17,133 @@ const getCell = ( name: string ) => { 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( <AlignmentMatrixControl /> ); + it( 'should render', async () => { + await renderAndInitCompositeStore( <AlignmentMatrixControl /> ); expect( getControl() ).toBeInTheDocument(); } ); + + it( 'should be centered by default', async () => { + const user = userEvent.setup(); + + await renderAndInitCompositeStore( <AlignmentMatrixControl /> ); + + 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( + <AlignmentMatrixControl + value="center" + onChange={ spy } + /> + ); + + 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( - <AlignmentMatrixControl value="center" onChange={ spy } /> - ); + it( 'unless already focused', async () => { + const user = userEvent.setup(); + const spy = jest.fn(); - await user.click( getCell( alignment ) ); + await renderAndInitCompositeStore( + <AlignmentMatrixControl + value="center" + onChange={ spy } + /> + ); - 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( + <AlignmentMatrixControl onChange={ spy } /> + ); + + 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( + <AlignmentMatrixControl onChange={ spy } /> + ); + + 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 ba5e113c42fc7..54455b61229b0 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/circular-option-picker/circular-option-picker-option.tsx b/packages/components/src/circular-option-picker/circular-option-picker-option.tsx index 2834228715115..7dcfd63555755 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 } - ></Button> + /> ); } @@ -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 ( <CompositeItem - { ...additionalProps } - { ...compositeState } - as={ Button } + render={ + <Button + { ...additionalProps } + role="option" + aria-selected={ !! isSelected } + ref={ forwardedRef } + /> + } + 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 ? ( <OptionAsOption { ...commonProps } - context={ compositeContext } + compositeStore={ compositeStore } isSelected={ isSelected } /> ) : ( 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 08c5ad94a34cb..047b0c569c7d3 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( <CircularOptionPickerContext.Provider value={ compositeContext }> <Composite { ...additionalProps } - { ...compositeState } + id={ baseId } + store={ compositeStore } role={ 'listbox' } > { options } @@ -134,10 +119,8 @@ function ButtonsCircularOptionPicker( const { actions, options, children, baseId, ...additionalProps } = props; return ( - <div { ...additionalProps }> - <CircularOptionPickerContext.Provider - value={ { isComposite: false, baseId } } - > + <div { ...additionalProps } id={ baseId }> + <CircularOptionPickerContext.Provider value={ { baseId } }> { options } { children } { actions } diff --git a/packages/components/src/circular-option-picker/types.ts b/packages/components/src/circular-option-picker/types.ts index cd9d9dca6be8d..519d81d590510 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 0000000000000..d329fd3fd11df --- /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/private-apis.ts b/packages/components/src/private-apis.ts index 38707860e1ce9..fd61c2564e6b0 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, From 030e2cf6a8307fba06c40be872e4de7c879734f0 Mon Sep 17 00:00:00 2001 From: Andrew Hayward <andrew.hayward@automattic.com> Date: Fri, 6 Oct 2023 08:41:40 +0100 Subject: [PATCH 20/20] Changing `Button` component toggled style selector (#55065) * Changing `Button` component toggled style selector Reverting change from class to attribute selector, until we can better assess how the `is-pressed` class is used elsewhere. * Updating CHANGELOG.md --- packages/components/CHANGELOG.md | 1 + packages/components/src/button/style.scss | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 7103151110e2f..14166f8827ec8 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -28,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 diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss index e3b8c55f074e8..03273056cfa17 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;