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

Yoast / wordpress-seo / e1f834bd916861c1418cb5c321609504f77c8e72

16 Jan 2026 02:08PM UTC coverage: 53.19% (+0.4%) from 52.832%
e1f834bd916861c1418cb5c321609504f77c8e72

Pull #22887

github

web-flow
Merge 2cb556c24 into 36a71a2d0
Pull Request #22887: Add a new key for step attribute and adapt the relevant code and make…

8774 of 16370 branches covered (53.6%)

Branch coverage included in aggregate %.

5 of 135 new or added lines in 14 files covered. (3.7%)

42 existing lines in 4 files now uncovered.

32905 of 61988 relevant lines covered (53.08%)

46516.26 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, 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 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
                        },
113
                } = this.props;
×
114

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
                        },
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 {func}   props.open Opens the media upload dialog.
143
         *
144
         * @returns {wp.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
         * The insert and remove step buttons.
160
         *
161
         * @returns {wp.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 {Component} 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 imageHtml = `<img class="wp-image-${ media.id }" alt="${ media.alt || "" }" src="${ media.url }" style="max-width: 100%;" />`;
×
233

234
                // Append to existing text (which is now a string).
NEW
235
                const newText = ( text || "" ) + imageHtml;
×
236

237
                this.props.onChange( name, newText, name, text, index );
×
238
        }
239

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

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

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

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

296
                const { id, name, text } = step;
×
297

298
                return (
×
299
                        <li className="schema-how-to-step" key={ id }>
300
                                <span className="schema-how-to-step-number">
301
                                        { isUnorderedList
×
302
                                                ? "•"
303
                                                : ( index + 1 ) + "."
304
                                        }
305
                                </span>
306
                                <RichText
307
                                        identifier={ `${ id }-name` }
308
                                        className="schema-how-to-step-name"
309
                                        tagName="p"
310
                                        key={ `${ id }-name` }
311
                                        value={ name }
312
                                        onChange={ this.onChangeTitle }
313
                                        onFocus={ this.onFocusTitle }
314
                                        // The unstableOnFocus prop is added for backwards compatibility with Gutenberg versions <= 15.1 (WordPress 6.2).
315
                                        unstableOnFocus={ 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
                                        // The unstableOnFocus prop is added for backwards compatibility with Gutenberg versions <= 15.1 (WordPress 6.2).
328
                                        unstableOnFocus={ this.onFocusText }
329
                                        placeholder={ __( "Enter a step description", "wordpress-seo" ) }
330
                                />
331
                                { isSelected &&
×
332
                                        <div className="schema-how-to-step-controls-container">
333
                                                { this.getMover() }
334
                                                { this.getButtons() }
335
                                        </div>
336
                                }
337
                        </li>
338
                );
339
        }
340
}
341

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

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