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

Yoast / wordpress-seo / 8c08d5692524db9d3692755cc04ce398e9f9fed4

22 Jan 2026 11:00AM UTC coverage: 53.352% (+0.009%) from 53.343%
8c08d5692524db9d3692755cc04ce398e9f9fed4

Pull #22887

github

web-flow
Merge e4cbdab67 into 52a2952fc
Pull Request #22887: Refactor FAQ and How-to blocks

8835 of 16367 branches covered (53.98%)

Branch coverage included in aggregate %.

62 of 135 new or added lines in 14 files covered. (45.93%)

2 existing lines in 2 files now uncovered.

32965 of 61980 relevant lines covered (53.19%)

46524.92 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, renderToString } from "@wordpress/element";
7
import { Button } from "@wordpress/components";
8
import { RichText, MediaUpload } from "@wordpress/block-editor";
9
import { convertToHTMLString, getImageSrc } from "../../shared-utils";
10

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 ) {
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 );
×
35
                this.onChangeText   = this.onChangeText.bind( this );
×
36
        }
37

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

47
        /**
48
         * Handles the remove step button action.
49
         *
50
         * @returns {void}
51
         */
52
        onRemoveStep() {
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 ) {
×
63
                        return;
×
64
                }
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 ) {
×
75
                        return;
×
76
                }
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() {
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() {
95
                this.props.onFocus( this.props.index, "text" );
×
96
        }
97

98
        /**
99
         * Handles the on change event in 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
                        },
113
                } = this.props;
×
114

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

118
        /**
119
         * Handles the on change event in 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
                        },
133
                } = this.props;
×
134

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 {Function}        props.open Opens the media upload dialog.
143
         *
144
         * @returns {JSX.Element} The media upload button.
145
         */
146
        getMediaUploadButton( props ) {
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
         * Gets the buttons for inserting and removing a step and for adding an image.
160
         *
161
         * @returns {JSX.Element} The buttons.
162
         */
163
        getButtons() {
164
                const {
165
                        step,
166
                } = this.props;
×
167

168
                return <div className="schema-how-to-step-button-container">
×
169
                        { ! 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 {JSX.Element} the buttons.
196
         */
197
        getMover() {
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
                        },
230
                } = this.props;
×
231

NEW
232
                const image = <img className={ `wp-image-${ media.id }` } alt={ media.alt || "" } src={ media.url } style={ { maxWidth: "100%" } } />;
×
233

234
                // Serializes the image element to string and append it to the existing text (which is now a string).
235
                // renderToString is used to convert the image element to an HTML string instead of just creating an image string manually,
236
                // to ensure safe and secure rendering. renderToString handles any necessary escaping and encoding that lowers the risk of XSS attacks.
NEW
237
                const newText = ( text || "" ) + renderToString( image );
×
238

239
                this.props.onChange( name, newText, name, text, index );
×
240
        }
241

242
        /**
243
         * Performs a shallow equal to prevent every step from being rerendered.
244
         *
245
         * @param {Object} nextProps The next props the component will receive.
246
         *
247
         * @returns {boolean} Whether or not the component should perform an update.
248
         */
249
        shouldComponentUpdate( nextProps ) {
250
                return ! isShallowEqualObjects( nextProps, this.props );
×
251
        }
252

253
        /**
254
         * Returns the component of the given How-to step to be rendered in a WordPress post
255
         * (e.g. not in the editor).
256
         *
257
         * @param {Object} step The how-to step.
258
         *
259
         * @returns {JSX.Element} The component to be rendered.
260
         */
261
        static Content( step ) {
NEW
262
                const { name, text, id } = step;
×
263
                // Backward compatibility for legacy array format.
NEW
264
                const stepName = Array.isArray( name ) ? convertToHTMLString( name ) : name;
×
NEW
265
                const stepText = Array.isArray( text ) ? convertToHTMLString( text ) : text;
×
266

UNCOV
267
                return (
×
268
                        <li className={ "schema-how-to-step" } id={ id } key={ id }>
269
                                <RichTextContentWithAppendedSpace
270
                                        tagName="strong"
271
                                        className="schema-how-to-step-name"
272
                                        key={ id + "-name" }
273
                                        value={ stepName }
274
                                />
275
                                <RichTextContentWithAppendedSpace
276
                                        tagName="p"
277
                                        className="schema-how-to-step-text"
278
                                        key={ id + "-text" }
279
                                        value={ stepText }
280
                                />
281
                        </li>
282
                );
283
        }
284

285
        /**
286
         * Renders this component.
287
         *
288
         * @returns {JSX.Element} The how-to step editor.
289
         */
290
        render() {
291
                const {
292
                        index,
293
                        step,
294
                        isSelected,
295
                        isUnorderedList,
296
                } = this.props;
×
297

298
                const { id, name, text } = step;
×
299

300
                return (
×
301
                        <li className="schema-how-to-step" key={ id }>
302
                                <span className="schema-how-to-step-number">
303
                                        { isUnorderedList
×
304
                                                ? "•"
305
                                                : ( index + 1 ) + "."
306
                                        }
307
                                </span>
308
                                <RichText
309
                                        identifier={ `${ id }-name` }
310
                                        className="schema-how-to-step-name"
311
                                        tagName="p"
312
                                        key={ `${ id }-name` }
313
                                        value={ name }
314
                                        onChange={ this.onChangeTitle }
315
                                        onFocus={ this.onFocusTitle }
316
                                        placeholder={ __( "Enter a step title", "wordpress-seo" ) }
317
                                        allowedFormats={ [ "core/italic", "core/strikethrough", "core/link", "core/annotation" ] }
318
                                />
319
                                <RichText
320
                                        identifier={ `${ id }-text` }
321
                                        className="schema-how-to-step-text"
322
                                        tagName="p"
323
                                        key={ `${ id }-text` }
324
                                        value={ text }
325
                                        onChange={ this.onChangeText }
326
                                        onFocus={ this.onFocusText }
327
                                        placeholder={ __( "Enter a step description", "wordpress-seo" ) }
328
                                />
329
                                { isSelected &&
×
330
                                        <div className="schema-how-to-step-controls-container">
331
                                                { this.getMover() }
332
                                                { this.getButtons() }
333
                                        </div>
334
                                }
335
                        </li>
336
                );
337
        }
338
}
339

340
HowToStep.propTypes = {
×
341
        index: PropTypes.number.isRequired,
342
        step: PropTypes.object.isRequired,
343
        onChange: PropTypes.func.isRequired,
344
        insertStep: PropTypes.func.isRequired,
345
        removeStep: PropTypes.func.isRequired,
346
        onFocus: PropTypes.func.isRequired,
347
        onMoveUp: PropTypes.func.isRequired,
348
        onMoveDown: PropTypes.func.isRequired,
349
        isSelected: PropTypes.bool.isRequired,
350
        isFirst: PropTypes.bool.isRequired,
351
        isLast: PropTypes.bool.isRequired,
352
        isUnorderedList: PropTypes.bool,
353
};
354

355
HowToStep.defaultProps = {
×
356
        isUnorderedList: false,
357
};
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