• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / eae09a5317839fc74ea9f16ed4e805d121db50bc

12 Jan 2026 12:44PM UTC coverage: 53.301% (-0.01%) from 53.315%
eae09a5317839fc74ea9f16ed4e805d121db50bc

Pull #22877

github

web-flow
Merge 58047efaf into 3d1950c99
Pull Request #22877: Convert RichText content to HTML string before rendering

8774 of 16284 branches covered (53.88%)

Branch coverage included in aggregate %.

0 of 9 new or added lines in 1 file covered. (0.0%)

131 existing lines in 3 files now uncovered.

32856 of 61819 relevant lines covered (53.15%)

46643.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/packages/js/src/structured-data-blocks/how-to/components/HowToStep.js
1
/* External dependencies */
2
import PropTypes from "prop-types";
3
import { __ } from "@wordpress/i18n";
4
import appendSpace from "../../../components/higherorder/appendSpace";
5
import { isShallowEqualObjects } from "@wordpress/is-shallow-equal";
6
import { Component } from "@wordpress/element";
7
import { Button } from "@wordpress/components";
8
import { RichText, MediaUpload } from "@wordpress/block-editor";
9
import { convertToHTMLString } from "../utils/convertToHTMLString";
10

UNCOV
11
const RichTextContentWithAppendedSpace = appendSpace( RichText.Content );
×
12

13
/**
14
 * A How-to step within a How-to block.
15
 */
16
export default class HowToStep extends Component {
17
        /**
18
         * Constructs a HowToStep editor component.
19
         *
20
         * @param {Object} props This component's properties.
21
         *
22
         * @returns {void}
23
         */
24
        constructor( props ) {
UNCOV
25
                super( props );
×
26

27
                this.onSelectImage  = this.onSelectImage.bind( this );
×
28
                this.onInsertStep   = this.onInsertStep.bind( this );
×
29
                this.onRemoveStep   = this.onRemoveStep.bind( this );
×
30
                this.onMoveStepUp   = this.onMoveStepUp.bind( this );
×
31
                this.onMoveStepDown = this.onMoveStepDown.bind( this );
×
32
                this.onFocusText    = this.onFocusText.bind( this );
×
33
                this.onFocusTitle   = this.onFocusTitle.bind( this );
×
34
                this.onChangeTitle  = this.onChangeTitle.bind( this );
×
UNCOV
35
                this.onChangeText   = this.onChangeText.bind( this );
×
36
        }
37

38
        /**
39
         * Handles the insert step button action.
40
         *
41
         * @returns {void}
42
         */
43
        onInsertStep() {
UNCOV
44
                this.props.insertStep( this.props.index );
×
45
        }
46

47
        /**
48
         * Handles the remove step button action.
49
         *
50
         * @returns {void}
51
         */
52
        onRemoveStep() {
UNCOV
53
                this.props.removeStep( this.props.index );
×
54
        }
55

56
        /**
57
         * Handles the move step up button action.
58
         *
59
         * @returns {void}
60
         */
61
        onMoveStepUp() {
62
                if ( this.props.isFirst ) {
×
UNCOV
63
                        return;
×
64
                }
UNCOV
65
                this.props.onMoveUp( this.props.index );
×
66
        }
67

68
        /**
69
         * Handles the move step down button action.
70
         *
71
         * @returns {void}
72
         */
73
        onMoveStepDown() {
74
                if ( this.props.isLast ) {
×
UNCOV
75
                        return;
×
76
                }
UNCOV
77
                this.props.onMoveDown( this.props.index );
×
78
        }
79

80
        /**
81
         * Handles the focus event on the title editor.
82
         *
83
         * @returns {void}
84
         */
85
        onFocusTitle() {
UNCOV
86
                this.props.onFocus( this.props.index, "name" );
×
87
        }
88

89
        /**
90
         * Handles the focus event on the text editor.
91
         *
92
         * @returns {void}
93
         */
94
        onFocusText() {
UNCOV
95
                this.props.onFocus( this.props.index, "text" );
×
96
        }
97

98
        /**
99
         * Handles the on change event on the title editor.
100
         *
101
         * @param {string} value The new title.
102
         *
103
         * @returns {void}
104
         */
105
        onChangeTitle( value ) {
106
                const {
107
                        onChange,
108
                        index,
109
                        step: {
110
                                text,
111
                                name,
112
                        },
UNCOV
113
                } = this.props;
×
114

UNCOV
115
                onChange( value, text, name, text, index );
×
116
        }
117

118
        /**
119
         * Handles the on change event on the text editor.
120
         *
121
         * @param {string} value The new text.
122
         *
123
         * @returns {void}
124
         */
125
        onChangeText( value ) {
126
                const {
127
                        onChange,
128
                        index,
129
                        step: {
130
                                text,
131
                                name,
132
                        },
UNCOV
133
                } = this.props;
×
134

UNCOV
135
                onChange( name, value, name, text, index );
×
136
        }
137

138
        /**
139
         * Renders the media upload button.
140
         *
141
         * @param {object} props      The receive props.
142
         * @param {func}   props.open Opens the media upload dialog.
143
         *
144
         * @returns {wp.Element} The media upload button.
145
         */
146
        getMediaUploadButton( props ) {
UNCOV
147
                return (
×
148
                        <Button
149
                                className="schema-how-to-step-button how-to-step-add-media"
150
                                icon="insert"
151
                                onClick={ props.open }
152
                        >
153
                                { __( "Add image", "wordpress-seo" ) }
154
                        </Button>
155
                );
156
        }
157

158
        /**
159
         * The insert and remove step buttons.
160
         *
161
         * @returns {wp.Element} The buttons.
162
         */
163
        getButtons() {
164
                const {
165
                        step,
UNCOV
166
                } = this.props;
×
167

UNCOV
168
                return <div className="schema-how-to-step-button-container">
×
169
                        { ! HowToStep.getImageSrc( step.text ) &&
×
170
                        <MediaUpload
171
                                onSelect={ this.onSelectImage }
172
                                allowedTypes={ [ "image" ] }
173
                                value={ step.id }
174
                                render={ this.getMediaUploadButton }
175
                        />
176
                        }
177
                        <Button
178
                                className="schema-how-to-step-button"
179
                                icon="trash"
180
                                label={ __( "Delete step", "wordpress-seo" ) }
181
                                onClick={ this.onRemoveStep }
182
                        />
183
                        <Button
184
                                className="schema-how-to-step-button"
185
                                icon="insert"
186
                                label={ __( "Insert step", "wordpress-seo" ) }
187
                                onClick={ this.onInsertStep }
188
                        />
189
                </div>;
190
        }
191

192
        /**
193
         * The mover buttons.
194
         *
195
         * @returns {Component} the buttons.
196
         */
197
        getMover() {
UNCOV
198
                return <div className="schema-how-to-step-mover">
×
199
                        <Button
200
                                className="editor-block-mover__control"
201
                                onClick={ this.onMoveStepUp }
202
                                icon="arrow-up-alt2"
203
                                label={ __( "Move step up", "wordpress-seo" ) }
204
                                aria-disabled={ this.props.isFirst }
205
                        />
206
                        <Button
207
                                className="editor-block-mover__control"
208
                                onClick={ this.onMoveStepDown }
209
                                icon="arrow-down-alt2"
210
                                label={ __( "Move step down", "wordpress-seo" ) }
211
                                aria-disabled={ this.props.isLast }
212
                        />
213
                </div>;
214
        }
215

216
        /**
217
         * Callback when an image from the media library has been selected.
218
         *
219
         * @param {Object} media The selected image.
220
         *
221
         * @returns {void}
222
         */
223
        onSelectImage( media ) {
224
                const {
225
                        index,
226
                        step: {
227
                                name,
228
                                text,
229
                        },
UNCOV
230
                } = this.props;
×
231

232
                let newText = text.slice();
×
UNCOV
233
                const image = <img className={ `wp-image-${ media.id }` } alt={ media.alt } src={ media.url } style="max-width:100%;" />;
×
234

235
                if ( newText.push ) {
×
UNCOV
236
                        newText.push( image );
×
237
                } else {
UNCOV
238
                        newText = [ newText, image ];
×
239
                }
240

UNCOV
241
                this.props.onChange( name, newText, name, text, index );
×
242
        }
243

244
        /**
245
         * Returns the image src from step contents.
246
         *
247
         * @param {array} contents The step contents.
248
         *
249
         * @returns {string|boolean} The image src or false if none is found.
250
         */
251
        static getImageSrc( contents ) {
252
                if ( ! contents || ! contents.filter ) {
×
UNCOV
253
                        return false;
×
254
                }
255

UNCOV
256
                const image = contents.filter( ( node ) => node && node.type && node.type === "img" )[ 0 ];
×
257

258
                if ( ! image ) {
×
UNCOV
259
                        return false;
×
260
                }
261

UNCOV
262
                return image.props.src;
×
263
        }
264

265
        /**
266
         * Perform a shallow equal to prevent every step from being rerendered.
267
         *
268
         * @param {object} nextProps The next props the component will receive.
269
         *
270
         * @returns {boolean} Whether or not the component should perform an update.
271
         */
272
        shouldComponentUpdate( nextProps ) {
UNCOV
273
                return ! isShallowEqualObjects( nextProps, this.props );
×
274
        }
275

276
        /**
277
         * Returns the component of the given How-to step to be rendered in a WordPress post
278
         * (e.g. not in the editor).
279
         *
280
         * @param {object} step The how-to step.
281
         *
282
         * @returns {wp.Element} The component to be rendered.
283
         */
284
        static Content( step ) {
UNCOV
285
                return (
×
286
                        <li className={ "schema-how-to-step" } id={ step.id } key={ step.id }>
287
                                <RichTextContentWithAppendedSpace
288
                                        tagName="strong"
289
                                        className="schema-how-to-step-name"
290
                                        key={ step.id + "-name" }
291
                                        value={ convertToHTMLString( step.name ) }
292
                                />
293
                                <RichTextContentWithAppendedSpace
294
                                        tagName="p"
295
                                        className="schema-how-to-step-text"
296
                                        key={ step.id + "-text" }
297
                                        value={ convertToHTMLString( step.text ) }
298
                                />
299
                        </li>
300
                );
301
        }
302

303
        /**
304
         * Renders this component.
305
         *
306
         * @returns {wp.Element} The how-to step editor.
307
         */
308
        render() {
309
                const {
310
                        index,
311
                        step,
312
                        isSelected,
313
                        isUnorderedList,
UNCOV
314
                } = this.props;
×
315

UNCOV
316
                const { id, name, text } = step;
×
317

UNCOV
318
                return (
×
319
                        <li className="schema-how-to-step" key={ id }>
320
                                <span className="schema-how-to-step-number">
321
                                        { isUnorderedList
×
322
                                                ? "•"
323
                                                : ( index + 1 ) + "."
324
                                        }
325
                                </span>
326
                                <RichText
327
                                        identifier={ `${ id }-name` }
328
                                        className="schema-how-to-step-name"
329
                                        tagName="p"
330
                                        key={ `${ id }-name` }
331
                                        value={ convertToHTMLString( name ) }
332
                                        onChange={ this.onChangeTitle }
333
                                        onFocus={ this.onFocusTitle }
334
                                        // The unstableOnFocus prop is added for backwards compatibility with Gutenberg versions <= 15.1 (WordPress 6.2).
335
                                        unstableOnFocus={ this.onFocusTitle }
336
                                        placeholder={ __( "Enter a step title", "wordpress-seo" ) }
337
                                        allowedFormats={ [ "core/italic", "core/strikethrough", "core/link", "core/annotation" ] }
338
                                />
339
                                <RichText
340
                                        identifier={ `${ id }-text` }
341
                                        className="schema-how-to-step-text"
342
                                        tagName="p"
343
                                        key={ `${ id }-text` }
344
                                        value={ convertToHTMLString( text ) }
345
                                        onChange={ this.onChangeText }
346
                                        onFocus={ this.onFocusText }
347
                                        // The unstableOnFocus prop is added for backwards compatibility with Gutenberg versions <= 15.1 (WordPress 6.2).
348
                                        unstableOnFocus={ this.onFocusText }
349
                                        placeholder={ __( "Enter a step description", "wordpress-seo" ) }
350
                                />
351
                                { isSelected &&
×
352
                                        <div className="schema-how-to-step-controls-container">
353
                                                { this.getMover() }
354
                                                { this.getButtons() }
355
                                        </div>
356
                                }
357
                        </li>
358
                );
359
        }
360
}
361

UNCOV
362
HowToStep.propTypes = {
×
363
        index: PropTypes.number.isRequired,
364
        step: PropTypes.object.isRequired,
365
        onChange: PropTypes.func.isRequired,
366
        insertStep: PropTypes.func.isRequired,
367
        removeStep: PropTypes.func.isRequired,
368
        onFocus: PropTypes.func.isRequired,
369
        onMoveUp: PropTypes.func.isRequired,
370
        onMoveDown: PropTypes.func.isRequired,
371
        isSelected: PropTypes.bool.isRequired,
372
        isFirst: PropTypes.bool.isRequired,
373
        isLast: PropTypes.bool.isRequired,
374
        isUnorderedList: PropTypes.bool,
375
};
376

UNCOV
377
HowToStep.defaultProps = {
×
378
        isUnorderedList: false,
379
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc