Skip to content

Commit ae633fd

Browse files
authored
Render HTML in post titles in visual mode and show raw HTML in code view (#54718)
* Revert "Strip HTML from Post Title when pasting (#35825)" This reverts commit 6d804a9. * Preserve changes * Fix util to be more aware of menu expanded state before acting * Remove redundant “rich” component Addresses feedback in #54718 (comment) * Add explaination around stripping HTML tags on paste
1 parent e93c392 commit ae633fd

11 files changed

Lines changed: 618 additions & 90 deletions

File tree

‎packages/e2e-test-utils/src/toggle-more-menu.js‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@
66
export async function toggleMoreMenu( waitFor ) {
77
const menuSelector = '.interface-more-menu-dropdown [aria-label="Options"]';
88

9+
const menuToggle = await page.waitForSelector( menuSelector );
10+
11+
const isOpen = await menuToggle.evaluate( ( el ) =>
12+
el.getAttribute( 'aria-expanded' )
13+
);
14+
15+
// If opening and it's already open then exit early.
16+
if ( isOpen === 'true' && waitFor === 'open' ) {
17+
return;
18+
}
19+
20+
// If closing and it's already closed then exit early.
21+
if ( isOpen === 'false' && waitFor === 'close' ) {
22+
return;
23+
}
24+
925
await page.click( menuSelector );
1026

1127
if ( waitFor ) {

‎packages/edit-post/src/components/text-editor/index.js‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
*/
44
import {
55
PostTextEditor,
6-
PostTitle,
6+
PostTitleRaw,
77
store as editorStore,
88
} from '@wordpress/editor';
99
import { Button } from '@wordpress/components';
1010
import { useDispatch, useSelect } from '@wordpress/data';
1111
import { __ } from '@wordpress/i18n';
1212
import { displayShortcut } from '@wordpress/keycodes';
13+
import { useEffect, useRef } from '@wordpress/element';
1314

1415
/**
1516
* Internal dependencies
@@ -22,6 +23,23 @@ export default function TextEditor() {
2223
}, [] );
2324
const { switchEditorMode } = useDispatch( editPostStore );
2425

26+
const { isWelcomeGuideVisible } = useSelect( ( select ) => {
27+
const { isFeatureActive } = select( editPostStore );
28+
29+
return {
30+
isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ),
31+
};
32+
}, [] );
33+
34+
const titleRef = useRef();
35+
36+
useEffect( () => {
37+
if ( isWelcomeGuideVisible ) {
38+
return;
39+
}
40+
titleRef?.current?.focus();
41+
}, [ isWelcomeGuideVisible ] );
42+
2543
return (
2644
<div className="edit-post-text-editor">
2745
{ isRichEditingEnabled && (
@@ -37,7 +55,7 @@ export default function TextEditor() {
3755
</div>
3856
) }
3957
<div className="edit-post-text-editor__body">
40-
<PostTitle />
58+
<PostTitleRaw ref={ titleRef } />
4159
<PostTextEditor />
4260
</div>
4361
</div>

‎packages/editor/src/components/index.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } fr
6767
export { default as PostTaxonomiesCheck } from './post-taxonomies/check';
6868
export { default as PostTextEditor } from './post-text-editor';
6969
export { default as PostTitle } from './post-title';
70+
export { default as PostTitleRaw } from './post-title/post-title-raw';
7071
export { default as PostTrash } from './post-trash';
7172
export { default as PostTrashCheck } from './post-trash/check';
7273
export { default as PostTypeSupportCheck } from './post-type-support-check';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const DEFAULT_CLASSNAMES =
2+
'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text';
3+
4+
export const REGEXP_NEWLINES = /[\r\n]+/g;

‎packages/editor/src/components/post-title/index.js‎

Lines changed: 57 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,12 @@ import classnames from 'classnames';
77
* WordPress dependencies
88
*/
99
import { __ } from '@wordpress/i18n';
10-
import {
11-
forwardRef,
12-
useEffect,
13-
useImperativeHandle,
14-
useRef,
15-
useState,
16-
} from '@wordpress/element';
10+
import { forwardRef, useState } from '@wordpress/element';
1711
import { decodeEntities } from '@wordpress/html-entities';
18-
import { ENTER } from '@wordpress/keycodes';
1912
import { useSelect, useDispatch } from '@wordpress/data';
20-
import { pasteHandler } from '@wordpress/blocks';
2113
import { store as blockEditorStore } from '@wordpress/block-editor';
14+
import { ENTER } from '@wordpress/keycodes';
15+
import { pasteHandler } from '@wordpress/blocks';
2216
import {
2317
__unstableUseRichText as useRichText,
2418
create,
@@ -31,78 +25,45 @@ import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
3125
/**
3226
* Internal dependencies
3327
*/
34-
import PostTypeSupportCheck from '../post-type-support-check';
3528
import { store as editorStore } from '../../store';
36-
37-
/**
38-
* Constants
39-
*/
40-
const REGEXP_NEWLINES = /[\r\n]+/g;
29+
import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants';
30+
import usePostTitleFocus from './use-post-title-focus';
31+
import usePostTitle from './use-post-title';
32+
import PostTypeSupportCheck from '../post-type-support-check';
4133

4234
function PostTitle( _, forwardedRef ) {
43-
const ref = useRef();
35+
const { placeholder, hasFixedToolbar } = useSelect( ( select ) => {
36+
const { getEditedPostAttribute } = select( editorStore );
37+
const { getSettings } = select( blockEditorStore );
38+
const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } =
39+
getSettings();
40+
41+
return {
42+
title: getEditedPostAttribute( 'title' ),
43+
placeholder: titlePlaceholder,
44+
hasFixedToolbar: _hasFixedToolbar,
45+
};
46+
}, [] );
47+
4448
const [ isSelected, setIsSelected ] = useState( false );
45-
const { editPost } = useDispatch( editorStore );
46-
const { insertDefaultBlock, clearSelectedBlock, insertBlocks } =
47-
useDispatch( blockEditorStore );
48-
const { isCleanNewPost, title, placeholder, hasFixedToolbar } = useSelect(
49-
( select ) => {
50-
const { getEditedPostAttribute, isCleanNewPost: _isCleanNewPost } =
51-
select( editorStore );
52-
const { getSettings } = select( blockEditorStore );
53-
const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } =
54-
getSettings();
55-
56-
return {
57-
isCleanNewPost: _isCleanNewPost(),
58-
title: getEditedPostAttribute( 'title' ),
59-
placeholder: titlePlaceholder,
60-
hasFixedToolbar: _hasFixedToolbar,
61-
};
62-
},
63-
[]
64-
);
6549

66-
useImperativeHandle( forwardedRef, () => ( {
67-
focus: () => {
68-
ref?.current?.focus();
69-
},
70-
} ) );
50+
const { ref: focusRef } = usePostTitleFocus( forwardedRef );
7151

72-
useEffect( () => {
73-
if ( ! ref.current ) {
74-
return;
75-
}
52+
const { title, setTitle: onUpdate } = usePostTitle();
7653

77-
const { defaultView } = ref.current.ownerDocument;
78-
const { name, parent } = defaultView;
79-
const ownerDocument =
80-
name === 'editor-canvas' ? parent.document : defaultView.document;
81-
const { activeElement, body } = ownerDocument;
82-
83-
// Only autofocus the title when the post is entirely empty. This should
84-
// only happen for a new post, which means we focus the title on new
85-
// post so the author can start typing right away, without needing to
86-
// click anything.
87-
if ( isCleanNewPost && ( ! activeElement || body === activeElement ) ) {
88-
ref.current.focus();
89-
}
90-
}, [ isCleanNewPost ] );
54+
const [ selection, setSelection ] = useState( {} );
9155

92-
function onEnterPress() {
93-
insertDefaultBlock( undefined, undefined, 0 );
56+
const { clearSelectedBlock, insertBlocks, insertDefaultBlock } =
57+
useDispatch( blockEditorStore );
58+
59+
function onChange( value ) {
60+
onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) );
9461
}
9562

9663
function onInsertBlockAfter( blocks ) {
9764
insertBlocks( blocks, 0 );
9865
}
9966

100-
function onUpdate( newTitle ) {
101-
editPost( { title: newTitle } );
102-
}
103-
104-
const [ selection, setSelection ] = useState( {} );
105-
10667
function onSelect() {
10768
setIsSelected( true );
10869
clearSelectedBlock();
@@ -113,8 +74,8 @@ function PostTitle( _, forwardedRef ) {
11374
setSelection( {} );
11475
}
11576

116-
function onChange( value ) {
117-
onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) );
77+
function onEnterPress() {
78+
insertDefaultBlock( undefined, undefined, 0 );
11879
}
11980

12081
function onKeyDown( event ) {
@@ -170,7 +131,13 @@ function PostTitle( _, forwardedRef ) {
170131
( firstBlock.name === 'core/heading' ||
171132
firstBlock.name === 'core/paragraph' )
172133
) {
173-
onUpdate( stripHTML( firstBlock.attributes.content ) );
134+
// Strip HTML to avoid unwanted HTML being added to the title.
135+
// In the majority of cases it is assumed that HTML in the title
136+
// is undesirable.
137+
const contentNoHTML = stripHTML(
138+
firstBlock.attributes.content
139+
);
140+
onUpdate( contentNoHTML );
174141
onInsertBlockAfter( content.slice( 1 ) );
175142
} else {
176143
onInsertBlockAfter( content );
@@ -180,10 +147,13 @@ function PostTitle( _, forwardedRef ) {
180147
...create( { html: title } ),
181148
...selection,
182149
};
183-
const newValue = insert(
184-
value,
185-
create( { html: stripHTML( content ) } )
186-
);
150+
151+
// Strip HTML to avoid unwanted HTML being added to the title.
152+
// In the majority of cases it is assumed that HTML in the title
153+
// is undesirable.
154+
const contentNoHTML = stripHTML( content );
155+
156+
const newValue = insert( value, create( { html: contentNoHTML } ) );
187157
onUpdate( toHTMLString( { value: newValue } ) );
188158
setSelection( {
189159
start: newValue.start,
@@ -192,21 +162,13 @@ function PostTitle( _, forwardedRef ) {
192162
}
193163
}
194164

195-
// The wp-block className is important for editor styles.
196-
// This same block is used in both the visual and the code editor.
197-
const className = classnames(
198-
'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text',
199-
{
200-
'is-selected': isSelected,
201-
'has-fixed-toolbar': hasFixedToolbar,
202-
}
203-
);
204165
const decodedPlaceholder =
205166
decodeEntities( placeholder ) || __( 'Add title' );
167+
206168
const { ref: richTextRef } = useRichText( {
207169
value: title,
208170
onChange,
209-
placeholder: decodedPlaceholder,
171+
decodedPlaceholder,
210172
selectionStart: selection.start,
211173
selectionEnd: selection.end,
212174
onSelectionChange( newStart, newEnd ) {
@@ -221,14 +183,21 @@ function PostTitle( _, forwardedRef ) {
221183
};
222184
} );
223185
},
224-
__unstableDisableFormats: true,
186+
__unstableDisableFormats: false,
187+
} );
188+
189+
// The wp-block className is important for editor styles.
190+
// This same block is used in both the visual and the code editor.
191+
const className = classnames( DEFAULT_CLASSNAMES, {
192+
'is-selected': isSelected,
193+
'has-fixed-toolbar': hasFixedToolbar,
225194
} );
226195

227-
/* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
228196
return (
197+
/* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
229198
<PostTypeSupportCheck supportKeys="title">
230199
<h1
231-
ref={ useMergeRefs( [ richTextRef, ref ] ) }
200+
ref={ useMergeRefs( [ richTextRef, focusRef ] ) }
232201
contentEditable
233202
className={ className }
234203
aria-label={ decodedPlaceholder }
@@ -241,8 +210,8 @@ function PostTitle( _, forwardedRef ) {
241210
onPaste={ onPaste }
242211
/>
243212
</PostTypeSupportCheck>
213+
/* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
244214
);
245-
/* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
246215
}
247216

248217
export default forwardRef( PostTitle );
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import classnames from 'classnames';
5+
6+
/**
7+
* WordPress dependencies
8+
*/
9+
import { TextareaControl } from '@wordpress/components';
10+
import { __ } from '@wordpress/i18n';
11+
import { decodeEntities } from '@wordpress/html-entities';
12+
import { useSelect } from '@wordpress/data';
13+
import { store as blockEditorStore } from '@wordpress/block-editor';
14+
import { useState, forwardRef } from '@wordpress/element';
15+
16+
/**
17+
* Internal dependencies
18+
*/
19+
import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants';
20+
import usePostTitleFocus from './use-post-title-focus';
21+
import usePostTitle from './use-post-title';
22+
23+
function PostTitleRaw( _, forwardedRef ) {
24+
const { placeholder, hasFixedToolbar } = useSelect( ( select ) => {
25+
const { getSettings } = select( blockEditorStore );
26+
const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } =
27+
getSettings();
28+
29+
return {
30+
placeholder: titlePlaceholder,
31+
hasFixedToolbar: _hasFixedToolbar,
32+
};
33+
}, [] );
34+
35+
const [ isSelected, setIsSelected ] = useState( false );
36+
37+
const { title, setTitle: onUpdate } = usePostTitle();
38+
const { ref: focusRef } = usePostTitleFocus( forwardedRef );
39+
40+
function onChange( value ) {
41+
onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) );
42+
}
43+
44+
function onSelect() {
45+
setIsSelected( true );
46+
}
47+
48+
function onUnselect() {
49+
setIsSelected( false );
50+
}
51+
52+
// The wp-block className is important for editor styles.
53+
// This same block is used in both the visual and the code editor.
54+
const className = classnames( DEFAULT_CLASSNAMES, {
55+
'is-selected': isSelected,
56+
'has-fixed-toolbar': hasFixedToolbar,
57+
'is-raw-text': true,
58+
} );
59+
60+
const decodedPlaceholder =
61+
decodeEntities( placeholder ) || __( 'Add title' );
62+
63+
return (
64+
<TextareaControl
65+
ref={ focusRef }
66+
value={ title }
67+
onChange={ onChange }
68+
onFocus={ onSelect }
69+
onBlur={ onUnselect }
70+
label={ placeholder }
71+
className={ className }
72+
placeholder={ decodedPlaceholder }
73+
hideLabelFromVision={ true }
74+
autoComplete="off"
75+
dir="auto"
76+
__nextHasNoMarginBottom
77+
/>
78+
);
79+
}
80+
81+
export default forwardRef( PostTitleRaw );

0 commit comments

Comments
 (0)