• 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/faq/components/Question.js
1
/* External dependencies */
2
import PropTypes from "prop-types";
3
import { __ } from "@wordpress/i18n";
4
import { isShallowEqualObjects } from "@wordpress/is-shallow-equal";
5
import { Component } from "@wordpress/element";
6
import { Button } from "@wordpress/components";
7
import { RichText, MediaUpload } from "@wordpress/block-editor";
8

9
/* Internal dependencies */
10
import appendSpace from "../../../components/higherorder/appendSpace";
11

12
const RichTextWithAppendedSpace = appendSpace( RichText.Content );
×
13

14
/**
15
 * A Question and answer pair within a FAQ block.
16
 */
17
export default class Question extends Component {
18
        /**
19
         * Constructs a Question editor component.
20
         *
21
         * @param {Object} props This component's props.
22
         *
23
         * @returns {void}
24
         */
25
        constructor( props ) {
26
                super( props );
×
27

28
                this.onSelectImage    = this.onSelectImage.bind( this );
×
29
                this.onFocusAnswer    = this.onFocusAnswer.bind( this );
×
30
                this.onFocusQuestion  = this.onFocusQuestion.bind( this );
×
31
                this.onChangeAnswer   = this.onChangeAnswer.bind( this );
×
32
                this.onChangeQuestion = this.onChangeQuestion.bind( this );
×
33
                this.onInsertQuestion = this.onInsertQuestion.bind( this );
×
34
                this.onRemoveQuestion = this.onRemoveQuestion.bind( this );
×
35
                this.onMoveDown       = this.onMoveDown.bind( this );
×
36
                this.onMoveUp         = this.onMoveUp.bind( this );
×
37
        }
38

39
        /**
40
         * Renders the media upload button.
41
         *
42
         * @param {Object}   props      The received props.
43
         * @param {function} props.open Opens the media upload dialog.
44
         *
45
         * @returns {wp.Element} The media upload button.
46
         */
47
        getMediaUploadButton( props ) {
48
                return (
×
49
                        <Button
50
                                className="schema-faq-section-button faq-section-add-media"
51
                                icon="insert"
52
                                onClick={ props.open }
53
                        >
54
                                { __( "Add image", "wordpress-seo" ) }
55
                        </Button>
56
                );
57
        }
58

59
        /**
60
         * Handle the focus event on the question editor.
61
         *
62
         * @returns {void}
63
         */
64
        onFocusQuestion() {
65
                this.props.onFocus( "question", this.props.index );
×
66
        }
67

68
        /**
69
         * Handle the focus event on the answer editor.
70
         *
71
         * @returns {void}
72
         */
73
        onFocusAnswer() {
74
                this.props.onFocus( "answer", this.props.index );
×
75
        }
76

77
        /**
78
         * Handles the on change event on the question editor.
79
         *
80
         * @param {string} value The new question.
81
         *
82
         * @returns {void}
83
         */
84
        onChangeQuestion( value ) {
85
                const {
86
                        index,
87
                        onChange,
88
                        attributes: {
89
                                answer,
90
                                question,
91
                        },
92
                } = this.props;
×
93

94
                onChange(
×
95
                        value,
96
                        answer,
97
                        question,
98
                        answer,
99
                        index
100
                );
101
        }
102

103
        /**
104
         * Handles the on change event on the answer editor.
105
         *
106
         * @param {string} value The new answer.
107
         *
108
         * @returns {void}
109
         */
110
        onChangeAnswer( value ) {
111
                const {
112
                        index,
113
                        onChange,
114
                        attributes: {
115
                                answer,
116
                                question,
117
                        },
118
                } = this.props;
×
119

120
                onChange(
×
121
                        question,
122
                        value,
123
                        question,
124
                        answer,
125
                        index
126
                );
127
        }
128

129
        /**
130
         * Handles the insert question button action.
131
         *
132
         * @returns {void}
133
         */
134
        onInsertQuestion() {
135
                this.props.insertQuestion( this.props.index );
×
136
        }
137

138
        /**
139
         * Handles the remove question button action.
140
         *
141
         * @returns {void}
142
         */
143
        onRemoveQuestion() {
144
                this.props.removeQuestion( this.props.index );
×
145
        }
146

147
        /**
148
         * Handle the move up button action.
149
         *
150
         * @returns {void}
151
         */
152
        onMoveUp() {
153
                if ( this.props.isFirst ) {
×
154
                        return;
×
155
                }
156

157
                this.props.onMoveUp( this.props.index );
×
158
        }
159
        /**
160
         * Handle the move down button action.
161
         *
162
         * @returns {void}
163
         */
164
        onMoveDown() {
165
                if ( this.props.isLast ) {
×
166
                        return;
×
167
                }
168

169
                this.props.onMoveDown( this.props.index );
×
170
        }
171

172
        /**
173
         * The insert and remove question buttons.
174
         *
175
         * @returns {Component} The buttons.
176
         */
177
        getButtons() {
178
                const {
179
                        attributes,
180
                } = this.props;
×
181

182
                return <div className="schema-faq-section-button-container">
×
183
                        <MediaUpload
184
                                onSelect={ this.onSelectImage }
185
                                allowedTypes={ [ "image" ] }
186
                                value={ attributes.id }
187
                                render={ this.getMediaUploadButton }
188
                        />
189
                        <Button
190
                                className="schema-faq-section-button"
191
                                icon="trash"
192
                                label={ __( "Delete question", "wordpress-seo" ) }
193
                                onClick={ this.onRemoveQuestion }
194
                        />
195
                        <Button
196
                                className="schema-faq-section-button"
197
                                icon="insert"
198
                                label={ __( "Insert question", "wordpress-seo" ) }
199
                                onClick={ this.onInsertQuestion }
200
                        />
201
                </div>;
202
        }
203

204
        /**
205
         * The mover buttons.
206
         *
207
         * @returns {Component} The buttons.
208
         */
209
        getMover() {
210
                return <div className="schema-faq-section-mover">
×
211
                        <Button
212
                                className="editor-block-mover__control"
213
                                onClick={ this.onMoveUp }
214
                                icon="arrow-up-alt2"
215
                                label={ __( "Move question up", "wordpress-seo" ) }
216
                                aria-disabled={ this.props.isFirst }
217
                        />
218
                        <Button
219
                                className="editor-block-mover__control"
220
                                onClick={ this.onMoveDown }
221
                                icon="arrow-down-alt2"
222
                                label={ __( "Move question down", "wordpress-seo" ) }
223
                                aria-disabled={ this.props.isLast }
224
                        />
225
                </div>;
226
        }
227

228
        /**
229
         * Callback when an image from the media library has been selected.
230
         *
231
         * @param {Object} media The selected image.
232
         *
233
         * @returns {void}
234
         */
235
        onSelectImage( media ) {
236
                const {
237
                        attributes: {
238
                                answer,
239
                                question,
240
                        },
241
                        index,
242
                } = this.props;
×
243

244
                let newAnswer = answer.slice();
×
245
                const image   = <img className={ `wp-image-${ media.id }` } alt={ media.alt } src={ media.url } style="max-width:100%;" />;
×
246

247
                if ( newAnswer.push ) {
×
248
                        newAnswer.push( image );
×
249
                } else {
250
                        newAnswer = [ newAnswer, image ];
×
251
                }
252

253
                this.props.onChange( question, newAnswer, question, answer, index );
×
254
        }
255

256
        /**
257
         * Returns the image src from step contents.
258
         *
259
         * @param {array} contents The step contents.
260
         *
261
         * @returns {string|boolean} The image src or false if none is found.
262
         */
263
        static getImageSrc( contents ) {
264
                if ( ! contents || ! contents.filter ) {
×
265
                        return false;
×
266
                }
267

268
                const image = contents.filter( ( node ) => node && node.type && node.type === "img" )[ 0 ];
×
269

270
                if ( ! image ) {
×
271
                        return false;
×
272
                }
273

274
                return image.props.src;
×
275
        }
276

277
        /**
278
         * Returns the component of the given question and answer to be rendered in a WordPress post
279
         * (e.g. not in the editor).
280
         *
281
         * @param {object} question The question and its answer.
282
         *
283
         * @returns {Component} The component to be rendered.
284
         */
285
        static Content( question ) {
286
                return (
×
287
                        <div className={ "schema-faq-section" } id={ question.id } key={ question.id }>
288
                                <RichTextWithAppendedSpace
289
                                        tagName="strong"
290
                                        className="schema-faq-question"
291
                                        key={ question.id + "-question" }
292
                                        value={ question.question }
293
                                />
294
                                <RichTextWithAppendedSpace
295
                                        tagName="p"
296
                                        className="schema-faq-answer"
297
                                        key={ question.id + "-answer" }
298
                                        value={ question.answer }
299
                                />
300
                        </div>
301
                );
302
        }
303

304
        /**
305
         * Perform a shallow equal to prevent every step from being rerendered.
306
         *
307
         * @param {object} nextProps The next props the component will receive.
308
         *
309
         * @returns {boolean} Whether or not the component should perform an update.
310
         */
311
        shouldComponentUpdate( nextProps ) {
312
                if ( ! isShallowEqualObjects( nextProps, this.props ) ) {
×
313
                        return true;
×
314
                }
UNCOV
315
                return false;
×
316
        }
317

318
        /**
319
         * Renders this component.
320
         *
321
         * @returns {Component} The how-to step editor.
322
         */
323
        render() {
324
                const {
325
                        attributes,
326
                        isSelected,
UNCOV
327
                } = this.props;
×
328

329
                const {
330
                        id,
331
                        question,
332
                        answer,
UNCOV
333
                } = attributes;
×
334

UNCOV
335
                return (
×
336
                        <div className="schema-faq-section" key={ id }>
337
                                <RichText
338
                                        identifier={ id + "-question" }
339
                                        className="schema-faq-question"
340
                                        tagName="p"
341
                                        key={ id + "-question" }
342
                                        value={ question }
343
                                        onChange={ this.onChangeQuestion }
344
                                        onFocus={ this.onFocusQuestion }
345
                                        // The unstableOnFocus prop is added for backwards compatibility with Gutenberg versions <= 15.1 (WordPress 6.2).
346
                                        unstableOnFocus={ this.onFocusQuestion }
347
                                        placeholder={ __( "Enter a question", "wordpress-seo" ) }
348
                                        allowedFormats={ [ "core/italic", "core/strikethrough", "core/link", "core/annotation" ] }
349
                                />
350
                                <RichText
351
                                        identifier={ id + "-answer" }
352
                                        className="schema-faq-answer"
353
                                        tagName="p"
354
                                        key={ id + "-answer" }
355
                                        value={ answer }
356
                                        onChange={ this.onChangeAnswer }
357
                                        onFocus={ this.onFocusAnswer }
358
                                        // The unstableOnFocus prop is added for backwards compatibility with Gutenberg versions <= 15.1 (WordPress 6.2).
359
                                        unstableOnFocus={ this.onFocusAnswer }
360
                                        placeholder={ __( "Enter the answer to the question", "wordpress-seo" ) }
361
                                />
362
                                { isSelected &&
×
363
                                        <div className="schema-faq-section-controls-container">
364
                                                { this.getMover() }
365
                                                { this.getButtons() }
366
                                        </div>
367
                                }
368
                        </div>
369
                );
370
        }
371
}
372

UNCOV
373
Question.propTypes = {
×
374
        index: PropTypes.number.isRequired,
375
        attributes: PropTypes.object.isRequired,
376
        onChange: PropTypes.func.isRequired,
377
        insertQuestion: PropTypes.func.isRequired,
378
        removeQuestion: PropTypes.func.isRequired,
379
        onFocus: PropTypes.func.isRequired,
380
        onMoveUp: PropTypes.func.isRequired,
381
        onMoveDown: PropTypes.func.isRequired,
382
        isSelected: PropTypes.bool.isRequired,
383
        isFirst: PropTypes.bool.isRequired,
384
        isLast: PropTypes.bool.isRequired,
385
};
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