diff --git a/packages/e2e-test-utils/src/toggle-more-menu.js b/packages/e2e-test-utils/src/toggle-more-menu.js
index fb7849c7990c91..67346e96cd21fa 100644
--- a/packages/e2e-test-utils/src/toggle-more-menu.js
+++ b/packages/e2e-test-utils/src/toggle-more-menu.js
@@ -6,6 +6,22 @@
export async function toggleMoreMenu( waitFor ) {
const menuSelector = '.interface-more-menu-dropdown [aria-label="Options"]';
+ const menuToggle = await page.waitForSelector( menuSelector );
+
+ const isOpen = await menuToggle.evaluate( ( el ) =>
+ el.getAttribute( 'aria-expanded' )
+ );
+
+ // If opening and it's already open then exit early.
+ if ( isOpen === 'true' && waitFor === 'open' ) {
+ return;
+ }
+
+ // If closing and it's already closed then exit early.
+ if ( isOpen === 'false' && waitFor === 'close' ) {
+ return;
+ }
+
await page.click( menuSelector );
if ( waitFor ) {
diff --git a/packages/edit-post/src/components/text-editor/index.js b/packages/edit-post/src/components/text-editor/index.js
index b4b2ad64133a82..871273c4a9e83c 100644
--- a/packages/edit-post/src/components/text-editor/index.js
+++ b/packages/edit-post/src/components/text-editor/index.js
@@ -3,13 +3,14 @@
*/
import {
PostTextEditor,
- PostTitle,
+ PostTitleRaw,
store as editorStore,
} from '@wordpress/editor';
import { Button } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { displayShortcut } from '@wordpress/keycodes';
+import { useEffect, useRef } from '@wordpress/element';
/**
* Internal dependencies
@@ -22,6 +23,23 @@ export default function TextEditor() {
}, [] );
const { switchEditorMode } = useDispatch( editPostStore );
+ const { isWelcomeGuideVisible } = useSelect( ( select ) => {
+ const { isFeatureActive } = select( editPostStore );
+
+ return {
+ isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ),
+ };
+ }, [] );
+
+ const titleRef = useRef();
+
+ useEffect( () => {
+ if ( isWelcomeGuideVisible ) {
+ return;
+ }
+ titleRef?.current?.focus();
+ }, [ isWelcomeGuideVisible ] );
+
return (
{ isRichEditingEnabled && (
@@ -37,7 +55,7 @@ export default function TextEditor() {
) }
diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js
index a3dfc991523225..5fefc5506a02fc 100644
--- a/packages/editor/src/components/index.js
+++ b/packages/editor/src/components/index.js
@@ -67,6 +67,7 @@ export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } fr
export { default as PostTaxonomiesCheck } from './post-taxonomies/check';
export { default as PostTextEditor } from './post-text-editor';
export { default as PostTitle } from './post-title';
+export { default as PostTitleRaw } from './post-title/post-title-raw';
export { default as PostTrash } from './post-trash';
export { default as PostTrashCheck } from './post-trash/check';
export { default as PostTypeSupportCheck } from './post-type-support-check';
diff --git a/packages/editor/src/components/post-title/constants.js b/packages/editor/src/components/post-title/constants.js
new file mode 100644
index 00000000000000..2b0ff197f2b9f1
--- /dev/null
+++ b/packages/editor/src/components/post-title/constants.js
@@ -0,0 +1,4 @@
+export const DEFAULT_CLASSNAMES =
+ 'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text';
+
+export const REGEXP_NEWLINES = /[\r\n]+/g;
diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js
index 09f5f30c2a660c..a61bc2f52eb842 100644
--- a/packages/editor/src/components/post-title/index.js
+++ b/packages/editor/src/components/post-title/index.js
@@ -7,18 +7,12 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import {
- forwardRef,
- useEffect,
- useImperativeHandle,
- useRef,
- useState,
-} from '@wordpress/element';
+import { forwardRef, useState } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
-import { ENTER } from '@wordpress/keycodes';
import { useSelect, useDispatch } from '@wordpress/data';
-import { pasteHandler } from '@wordpress/blocks';
import { store as blockEditorStore } from '@wordpress/block-editor';
+import { ENTER } from '@wordpress/keycodes';
+import { pasteHandler } from '@wordpress/blocks';
import {
__unstableUseRichText as useRichText,
create,
@@ -31,78 +25,45 @@ import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
/**
* Internal dependencies
*/
-import PostTypeSupportCheck from '../post-type-support-check';
import { store as editorStore } from '../../store';
-
-/**
- * Constants
- */
-const REGEXP_NEWLINES = /[\r\n]+/g;
+import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants';
+import usePostTitleFocus from './use-post-title-focus';
+import usePostTitle from './use-post-title';
+import PostTypeSupportCheck from '../post-type-support-check';
function PostTitle( _, forwardedRef ) {
- const ref = useRef();
+ const { placeholder, hasFixedToolbar } = useSelect( ( select ) => {
+ const { getEditedPostAttribute } = select( editorStore );
+ const { getSettings } = select( blockEditorStore );
+ const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } =
+ getSettings();
+
+ return {
+ title: getEditedPostAttribute( 'title' ),
+ placeholder: titlePlaceholder,
+ hasFixedToolbar: _hasFixedToolbar,
+ };
+ }, [] );
+
const [ isSelected, setIsSelected ] = useState( false );
- const { editPost } = useDispatch( editorStore );
- const { insertDefaultBlock, clearSelectedBlock, insertBlocks } =
- useDispatch( blockEditorStore );
- const { isCleanNewPost, title, placeholder, hasFixedToolbar } = useSelect(
- ( select ) => {
- const { getEditedPostAttribute, isCleanNewPost: _isCleanNewPost } =
- select( editorStore );
- const { getSettings } = select( blockEditorStore );
- const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } =
- getSettings();
-
- return {
- isCleanNewPost: _isCleanNewPost(),
- title: getEditedPostAttribute( 'title' ),
- placeholder: titlePlaceholder,
- hasFixedToolbar: _hasFixedToolbar,
- };
- },
- []
- );
- useImperativeHandle( forwardedRef, () => ( {
- focus: () => {
- ref?.current?.focus();
- },
- } ) );
+ const { ref: focusRef } = usePostTitleFocus( forwardedRef );
- useEffect( () => {
- if ( ! ref.current ) {
- return;
- }
+ const { title, setTitle: onUpdate } = usePostTitle();
- const { defaultView } = ref.current.ownerDocument;
- const { name, parent } = defaultView;
- const ownerDocument =
- name === 'editor-canvas' ? parent.document : defaultView.document;
- const { activeElement, body } = ownerDocument;
-
- // Only autofocus the title when the post is entirely empty. This should
- // only happen for a new post, which means we focus the title on new
- // post so the author can start typing right away, without needing to
- // click anything.
- if ( isCleanNewPost && ( ! activeElement || body === activeElement ) ) {
- ref.current.focus();
- }
- }, [ isCleanNewPost ] );
+ const [ selection, setSelection ] = useState( {} );
- function onEnterPress() {
- insertDefaultBlock( undefined, undefined, 0 );
+ const { clearSelectedBlock, insertBlocks, insertDefaultBlock } =
+ useDispatch( blockEditorStore );
+
+ function onChange( value ) {
+ onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) );
}
function onInsertBlockAfter( blocks ) {
insertBlocks( blocks, 0 );
}
- function onUpdate( newTitle ) {
- editPost( { title: newTitle } );
- }
-
- const [ selection, setSelection ] = useState( {} );
-
function onSelect() {
setIsSelected( true );
clearSelectedBlock();
@@ -113,8 +74,8 @@ function PostTitle( _, forwardedRef ) {
setSelection( {} );
}
- function onChange( value ) {
- onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) );
+ function onEnterPress() {
+ insertDefaultBlock( undefined, undefined, 0 );
}
function onKeyDown( event ) {
@@ -170,7 +131,13 @@ function PostTitle( _, forwardedRef ) {
( firstBlock.name === 'core/heading' ||
firstBlock.name === 'core/paragraph' )
) {
- onUpdate( stripHTML( firstBlock.attributes.content ) );
+ // Strip HTML to avoid unwanted HTML being added to the title.
+ // In the majority of cases it is assumed that HTML in the title
+ // is undesirable.
+ const contentNoHTML = stripHTML(
+ firstBlock.attributes.content
+ );
+ onUpdate( contentNoHTML );
onInsertBlockAfter( content.slice( 1 ) );
} else {
onInsertBlockAfter( content );
@@ -180,10 +147,13 @@ function PostTitle( _, forwardedRef ) {
...create( { html: title } ),
...selection,
};
- const newValue = insert(
- value,
- create( { html: stripHTML( content ) } )
- );
+
+ // Strip HTML to avoid unwanted HTML being added to the title.
+ // In the majority of cases it is assumed that HTML in the title
+ // is undesirable.
+ const contentNoHTML = stripHTML( content );
+
+ const newValue = insert( value, create( { html: contentNoHTML } ) );
onUpdate( toHTMLString( { value: newValue } ) );
setSelection( {
start: newValue.start,
@@ -192,21 +162,13 @@ function PostTitle( _, forwardedRef ) {
}
}
- // The wp-block className is important for editor styles.
- // This same block is used in both the visual and the code editor.
- const className = classnames(
- 'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text',
- {
- 'is-selected': isSelected,
- 'has-fixed-toolbar': hasFixedToolbar,
- }
- );
const decodedPlaceholder =
decodeEntities( placeholder ) || __( 'Add title' );
+
const { ref: richTextRef } = useRichText( {
value: title,
onChange,
- placeholder: decodedPlaceholder,
+ decodedPlaceholder,
selectionStart: selection.start,
selectionEnd: selection.end,
onSelectionChange( newStart, newEnd ) {
@@ -221,14 +183,21 @@ function PostTitle( _, forwardedRef ) {
};
} );
},
- __unstableDisableFormats: true,
+ __unstableDisableFormats: false,
+ } );
+
+ // The wp-block className is important for editor styles.
+ // This same block is used in both the visual and the code editor.
+ const className = classnames( DEFAULT_CLASSNAMES, {
+ 'is-selected': isSelected,
+ 'has-fixed-toolbar': hasFixedToolbar,
} );
- /* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
return (
+ /* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
+ /* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
);
- /* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
}
export default forwardRef( PostTitle );
diff --git a/packages/editor/src/components/post-title/post-title-raw.js b/packages/editor/src/components/post-title/post-title-raw.js
new file mode 100644
index 00000000000000..b6a52e43731926
--- /dev/null
+++ b/packages/editor/src/components/post-title/post-title-raw.js
@@ -0,0 +1,81 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { TextareaControl } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { decodeEntities } from '@wordpress/html-entities';
+import { useSelect } from '@wordpress/data';
+import { store as blockEditorStore } from '@wordpress/block-editor';
+import { useState, forwardRef } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants';
+import usePostTitleFocus from './use-post-title-focus';
+import usePostTitle from './use-post-title';
+
+function PostTitleRaw( _, forwardedRef ) {
+ const { placeholder, hasFixedToolbar } = useSelect( ( select ) => {
+ const { getSettings } = select( blockEditorStore );
+ const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } =
+ getSettings();
+
+ return {
+ placeholder: titlePlaceholder,
+ hasFixedToolbar: _hasFixedToolbar,
+ };
+ }, [] );
+
+ const [ isSelected, setIsSelected ] = useState( false );
+
+ const { title, setTitle: onUpdate } = usePostTitle();
+ const { ref: focusRef } = usePostTitleFocus( forwardedRef );
+
+ function onChange( value ) {
+ onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) );
+ }
+
+ function onSelect() {
+ setIsSelected( true );
+ }
+
+ function onUnselect() {
+ setIsSelected( false );
+ }
+
+ // The wp-block className is important for editor styles.
+ // This same block is used in both the visual and the code editor.
+ const className = classnames( DEFAULT_CLASSNAMES, {
+ 'is-selected': isSelected,
+ 'has-fixed-toolbar': hasFixedToolbar,
+ 'is-raw-text': true,
+ } );
+
+ const decodedPlaceholder =
+ decodeEntities( placeholder ) || __( 'Add title' );
+
+ return (
+
+ );
+}
+
+export default forwardRef( PostTitleRaw );
diff --git a/packages/editor/src/components/post-title/style.scss b/packages/editor/src/components/post-title/style.scss
new file mode 100644
index 00000000000000..bf667c39933bdf
--- /dev/null
+++ b/packages/editor/src/components/post-title/style.scss
@@ -0,0 +1,4 @@
+.edit-post-text-editor__body .is-raw-text textarea {
+ font-size: inherit;
+ line-height: inherit;
+}
diff --git a/packages/editor/src/components/post-title/use-post-title-focus.js b/packages/editor/src/components/post-title/use-post-title-focus.js
new file mode 100644
index 00000000000000..effac53f2670a2
--- /dev/null
+++ b/packages/editor/src/components/post-title/use-post-title-focus.js
@@ -0,0 +1,50 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useImperativeHandle, useRef } from '@wordpress/element';
+import { useSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { store as editorStore } from '../../store';
+
+export default function usePostTitleFocus( forwardedRef ) {
+ const ref = useRef();
+
+ const { isCleanNewPost } = useSelect( ( select ) => {
+ const { isCleanNewPost: _isCleanNewPost } = select( editorStore );
+
+ return {
+ isCleanNewPost: _isCleanNewPost(),
+ };
+ }, [] );
+
+ useImperativeHandle( forwardedRef, () => ( {
+ focus: () => {
+ ref?.current?.focus();
+ },
+ } ) );
+
+ useEffect( () => {
+ if ( ! ref.current ) {
+ return;
+ }
+
+ const { defaultView } = ref.current.ownerDocument;
+ const { name, parent } = defaultView;
+ const ownerDocument =
+ name === 'editor-canvas' ? parent.document : defaultView.document;
+ const { activeElement, body } = ownerDocument;
+
+ // Only autofocus the title when the post is entirely empty. This should
+ // only happen for a new post, which means we focus the title on new
+ // post so the author can start typing right away, without needing to
+ // click anything.
+ if ( isCleanNewPost && ( ! activeElement || body === activeElement ) ) {
+ ref.current.focus();
+ }
+ }, [ isCleanNewPost ] );
+
+ return { ref };
+}
diff --git a/packages/editor/src/components/post-title/use-post-title.js b/packages/editor/src/components/post-title/use-post-title.js
new file mode 100644
index 00000000000000..65bd67af6fb4c8
--- /dev/null
+++ b/packages/editor/src/components/post-title/use-post-title.js
@@ -0,0 +1,25 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect, useDispatch } from '@wordpress/data';
+/**
+ * Internal dependencies
+ */
+import { store as editorStore } from '../../store';
+
+export default function usePostTitle() {
+ const { editPost } = useDispatch( editorStore );
+ const { title } = useSelect( ( select ) => {
+ const { getEditedPostAttribute } = select( editorStore );
+
+ return {
+ title: getEditedPostAttribute( 'title' ),
+ };
+ }, [] );
+
+ function updateTitle( newTitle ) {
+ editPost( { title: newTitle } );
+ }
+
+ return { title, setTitle: updateTitle };
+}
diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss
index ccc26e23b430b0..986cb645c271f5 100644
--- a/packages/editor/src/style.scss
+++ b/packages/editor/src/style.scss
@@ -17,6 +17,7 @@
@import "./components/post-sync-status/style.scss";
@import "./components/post-taxonomies/style.scss";
@import "./components/post-text-editor/style.scss";
+@import "./components/post-title/style.scss";
@import "./components/post-url/style.scss";
@import "./components/post-visibility/style.scss";
@import "./components/post-trash/style.scss";
diff --git a/test/e2e/specs/editor/various/post-title.spec.js b/test/e2e/specs/editor/various/post-title.spec.js
new file mode 100644
index 00000000000000..3c2330d1684d03
--- /dev/null
+++ b/test/e2e/specs/editor/various/post-title.spec.js
@@ -0,0 +1,359 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Post title', () => {
+ test.describe( 'Focus handling', () => {
+ test( 'should focus on the post title field when creating a new post in visual mode', async ( {
+ editor,
+ admin,
+ } ) => {
+ await admin.createNewPost();
+
+ const pageTitleField = editor.canvas.getByRole( 'textbox', {
+ name: 'Add title',
+ } );
+
+ await expect( pageTitleField ).toBeFocused();
+ } );
+
+ test( 'should focus on the post title field when creating a new post in code editor mode', async ( {
+ page,
+
+ admin,
+ pageUtils,
+ } ) => {
+ await admin.createNewPost();
+
+ // switch Editor to code editor mode
+ // Open code editor
+ await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor
+
+ // Check we're in Code view mode.
+ await expect(
+ page.getByRole( 'heading', {
+ name: 'Editing code',
+ } )
+ ).toBeVisible();
+
+ const pageTitleField = page.getByRole( 'textbox', {
+ name: 'Add title',
+ } );
+
+ await expect( pageTitleField ).toBeFocused();
+ } );
+ } );
+ test.describe( 'HTML handling', () => {
+ test( `should (visually) render any HTML in Post Editor's post title field when in Visual editing mode`, async ( {
+ page,
+ editor,
+ admin,
+ requestUtils,
+ } ) => {
+ const { id: postId } = await requestUtils.createPost( {
+ title: 'I am emphasis I am bold I am anchor',
+ content: 'Hello world',
+ status: 'publish',
+ } );
+
+ await admin.visitAdminPage(
+ 'post.php',
+ `post=${ postId }&action=edit`
+ );
+
+ await page.evaluate( () => {
+ window.wp.data
+ .dispatch( 'core/preferences' )
+ .set( 'core/edit-post', 'welcomeGuide', false );
+
+ window.wp.data
+ .dispatch( 'core/preferences' )
+ .set( 'core/edit-post', 'fullscreenMode', false );
+ }, false );
+
+ const pageTitleField = editor.canvas.getByRole( 'textbox', {
+ name: 'Add title',
+ } );
+
+ await expect( pageTitleField ).toHaveText(
+ 'I am emphasis I am bold I am anchor'
+ );
+
+ // Check the HTML elements have been **rendered** rather than
+ // output in raw form.
+ await expect( pageTitleField.locator( 'css=em' ) ).toHaveText(
+ 'emphasis'
+ );
+
+ await expect( pageTitleField.locator( 'css=strong' ) ).toHaveText(
+ 'bold'
+ );
+
+ await expect( pageTitleField.locator( 'css=a' ) ).toHaveText(
+ 'anchor'
+ );
+ } );
+
+ test( `should show raw HTML in the post title field when in Code view mode `, async ( {
+ page,
+ admin,
+ requestUtils,
+ pageUtils,
+ } ) => {
+ const { id: postId } = await requestUtils.createPost( {
+ title: 'I am emphasis I am bold I am anchor',
+ content: 'Hello world',
+ status: 'publish',
+ } );
+
+ await admin.visitAdminPage(
+ 'post.php',
+ `post=${ postId }&action=edit`
+ );
+
+ await page.evaluate( () => {
+ window.wp.data
+ .dispatch( 'core/preferences' )
+ .set( 'core/edit-post', 'welcomeGuide', false );
+
+ window.wp.data
+ .dispatch( 'core/preferences' )
+ .set( 'core/edit-post', 'fullscreenMode', false );
+ }, false );
+
+ // switch Editor to code editor mode
+ // Open code editor
+ await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor
+
+ // Check we're in Code view mode.
+ await expect(
+ page.getByRole( 'heading', {
+ name: 'Editing code',
+ } )
+ ).toBeVisible();
+
+ const codeViewPageTitleField = page.getByRole( 'textbox', {
+ name: 'Add title',
+ } );
+
+ // Check that the pageTitleField has the raw HTML
+ await expect( codeViewPageTitleField ).toHaveText(
+ 'I am emphasis I am bold I am anchor'
+ );
+ } );
+
+ test( 'should strip HTML tags when pasting string of HTML into the post title field in Visual mode', async ( {
+ editor,
+ admin,
+ pageUtils,
+ } ) => {
+ await admin.createNewPost();
+
+ const pageTitleField = editor.canvas.getByRole( 'textbox', {
+ name: 'Add title',
+ } );
+
+ await expect( pageTitleField ).toBeFocused();
+
+ pageUtils.setClipboardData( {
+ html: 'I am emphasis I am bold I am anchor',
+ } );
+ await pageUtils.pressKeys( 'primary+v' );
+
+ await expect( pageTitleField ).toHaveText(
+ 'I am emphasis I am bold I am anchor'
+ );
+
+ // Check the HTML elements have been stripped and are not rendered.
+ await expect( pageTitleField.locator( 'css=em' ) ).toBeHidden();
+
+ await expect( pageTitleField.locator( 'css=strong' ) ).toBeHidden();
+
+ await expect( pageTitleField.locator( 'css=a' ) ).toBeHidden();
+ } );
+
+ // Reinstate once the PR to fix paste events is merged:
+ // https://github.com/WordPress/gutenberg/pull/55030.
+ // eslint-disable-next-line playwright/no-skipped-test
+ test.skip( 'should retain HTML tags when pasting string of HTML into the post title field in Code view mode', async ( {
+ page,
+ admin,
+ pageUtils,
+ } ) => {
+ await admin.createNewPost();
+
+ // switch Editor to code editor mode
+ // Open code editor
+ await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor
+
+ // Check we're in Code view mode.
+ await expect(
+ page.getByRole( 'heading', {
+ name: 'Editing code',
+ } )
+ ).toBeVisible();
+
+ const pageTitleField = page.getByRole( 'textbox', {
+ name: 'Add title',
+ } );
+
+ pageUtils.setClipboardData( {
+ plainText:
+ 'I am emphasis I am bold I am anchor',
+ html: 'I am emphasis I am bold I am anchor',
+ } );
+
+ // focus on the title field
+ await pageTitleField.focus();
+
+ await pageUtils.pressKeys( 'primary+v' );
+
+ await expect( pageTitleField ).toHaveText(
+ 'I am emphasis I am bold I am anchor'
+ );
+ } );
+
+ test( 'should strip HTML tags from Post Title when pasted text is transformed to blocks', async ( {
+ editor,
+ admin,
+ pageUtils,
+ } ) => {
+ await admin.createNewPost();
+
+ const pageTitleField = editor.canvas.getByRole( 'textbox', {
+ name: 'Add title',
+ } );
+
+ await expect( pageTitleField ).toBeFocused();
+
+ // This HTML will ultimately be parsed into two blocks
+ // The first will have it's `content` attribute stripped of HTML
+ // and used as the Page Title.
+ // The second will be inserted into the post contents and will
+ // retain its HTML.
+ pageUtils.setClipboardData( {
+ html: `
+ I am heading block title with HTML tag
+ And I am the rest of titles with emphasis tag!
+ `,
+ } );
+ await pageUtils.pressKeys( 'primary+v' );
+
+ // Check the HTML elements have been stripped from the first block's
+ // `content` attribute...
+ await expect( pageTitleField ).toHaveText(
+ 'I am heading block title with HTML tag'
+ );
+
+ // ...and are not rendered.
+ await expect( pageTitleField.locator( 'css=strong' ) ).toBeHidden();
+
+ // Check the 2nd block ended up in the post contents and did not
+ // have its HTML stripped out.
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/paragraph',
+ attributes: {
+ content:
+ 'And I am the rest of titles with emphasis tag!',
+ },
+ },
+ ] );
+ } );
+
+ test( 'should output HTML tags in plaintext when added into Post Title field in visual editor mode', async ( {
+ editor,
+ page,
+ admin,
+ pageUtils,
+ } ) => {
+ await admin.createNewPost();
+
+ const pageTitleField = editor.canvas.getByRole( 'textbox', {
+ name: 'Add title',
+ } );
+
+ await expect( pageTitleField ).toBeFocused();
+
+ await page.keyboard.type( 'I am emphasis' );
+
+ // Expect that manually inputting HTML does not result in any
+ // unexpected transformations into rendered output.
+ await expect( pageTitleField ).toHaveText(
+ 'I am emphasis'
+ );
+
+ // Check that the `em` tag was output in plaintext and not rendered.
+ await expect( pageTitleField.locator( 'css=em' ) ).toBeHidden();
+
+ // Switch to code view
+ await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor
+
+ const codeViewPageTitleField = page.getByRole( 'textbox', {
+ name: 'Add title',
+ } );
+
+ // Check that the `em` tag was output in plaintext (HTML entities)
+ // Note that the `>` is not required to be converted to entity form
+ // (see https://github.com/WordPress/gutenberg/pull/54718/files#r1347124685).
+ await expect( codeViewPageTitleField ).toHaveText(
+ 'I am <em>emphasis</em>'
+ );
+ } );
+
+ test( 'should output HTML tags in plaintext in visual editor mode when HTML is added in plaintext in code editor mode', async ( {
+ editor,
+ page,
+ admin,
+ pageUtils,
+ } ) => {
+ await admin.createNewPost();
+
+ // switch Editor to code editor mode
+ // Open code editor
+ await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor
+
+ // Check we're in Code view mode.
+ await expect(
+ page.getByRole( 'heading', {
+ name: 'Editing code',
+ } )
+ ).toBeVisible();
+
+ const codeViewPageTitleField = page.getByRole( 'textbox', {
+ name: 'Add title',
+ } );
+
+ await codeViewPageTitleField.focus();
+
+ // Also verifies that the field handles typing into the field.
+ await page.keyboard.type( 'I am <em>emphasis</em>' );
+
+ await expect( codeViewPageTitleField ).toHaveText(
+ 'I am <em>emphasis</em>'
+ );
+
+ // Switch to visual view
+ await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor
+
+ const visualViewPageTitleField = editor.canvas.getByRole(
+ 'textbox',
+ {
+ name: 'Add title',
+ editable: 'richtext',
+ }
+ );
+
+ // Check that the `em` tag was output in plaintext
+ await expect( visualViewPageTitleField ).toHaveText(
+ 'I am emphasis'
+ );
+
+ // Check that no HTML tags were rendered.
+ await expect(
+ visualViewPageTitleField.locator( 'css=em' )
+ ).toBeHidden();
+ } );
+ } );
+} );