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

Yoast / wordpress-seo / 3d7d6f6b2bab9604d7575991225a4a9178614e04

22 Jan 2026 01:34PM UTC coverage: 42.145%. First build
3d7d6f6b2bab9604d7575991225a4a9178614e04

Pull #22887

github

web-flow
Merge 5f73610dd into 51865041f
Pull Request #22887: Refactor FAQ and How-to blocks

2732 of 9905 branches covered (27.58%)

Branch coverage included in aggregate %.

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

23283 of 51822 relevant lines covered (44.93%)

4.79 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, renderToString } 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
import { convertToHTMLString } from "../../shared-utils";
12

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

173
        /**
174
         * Gets the buttons for inserting and removing question and for uploading images.
175
         *
176
         * @returns {JSX.Element} The buttons.
177
         */
178
        getButtons() {
179
                const {
180
                        attributes,
181
                } = this.props;
×
182

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

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

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

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

247
                // Serializes the image element to string and append it to the existing answer (which is now a string).
248
                // renderToString is used to convert the image element to an HTML string instead of just creating an image string manually,
249
                // to ensure safe and secure rendering. renderToString handles any necessary escaping and encoding that lowers the risk of XSS attacks.
NEW
250
                const newAnswer = ( answer || "" ) + renderToString( image );
×
251

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

255
        /**
256
         * Returns the component of the given question and answer to be rendered in a WordPress post
257
         * (e.g. not in the editor).
258
         *
259
         * @param {Object} question The question and its answer.
260
         *
261
         * @returns {JSX.Element} The component to be rendered.
262
         */
263
        static Content( question ) {
264
                // Backwards compatibility for questions and answers stored as arrays.
NEW
265
                const questionItem = Array.isArray( question.question ) ? convertToHTMLString( question.question ) : question.question;
×
NEW
266
                const answerItem = Array.isArray( question.answer ) ? convertToHTMLString( question.answer ) : question.answer;
×
267
                return (
×
268
                        <div className={ "schema-faq-section" } id={ question.id } key={ question.id }>
269
                                <RichTextWithAppendedSpace
270
                                        tagName="strong"
271
                                        className="schema-faq-question"
272
                                        key={ question.id + "-question" }
273
                                        value={ questionItem }
274
                                />
275
                                <RichTextWithAppendedSpace
276
                                        tagName="p"
277
                                        className="schema-faq-answer"
278
                                        key={ question.id + "-answer" }
279
                                        value={ answerItem }
280
                                />
281
                        </div>
282
                );
283
        }
284

285
        /**
286
         * Performs a shallow equal to prevent every question from being rerendered.
287
         *
288
         * @param {Object} nextProps The next props the component will receive.
289
         *
290
         * @returns {boolean} Whether or not the component should perform an update.
291
         */
292
        shouldComponentUpdate( nextProps ) {
293
                return ! isShallowEqualObjects( nextProps, this.props );
×
294
        }
295

296
        /**
297
         * Renders this component.
298
         *
299
         * @returns {JSX.Element} The FAQ question editor.
300
         */
301
        render() {
302
                const {
303
                        attributes,
304
                        isSelected,
305
                } = this.props;
×
306

307
                const {
308
                        id,
309
                        question,
310
                        answer,
311
                } = attributes;
×
312

313
                return (
×
314
                        <div className="schema-faq-section" key={ id }>
315
                                <RichText
316
                                        identifier={ id + "-question" }
317
                                        className="schema-faq-question"
318
                                        tagName="p"
319
                                        key={ id + "-question" }
320
                                        value={ question }
321
                                        onChange={ this.onChangeQuestion }
322
                                        onFocus={ this.onFocusQuestion }
323
                                        placeholder={ __( "Enter a question", "wordpress-seo" ) }
324
                                        allowedFormats={ [ "core/italic", "core/strikethrough", "core/link", "core/annotation" ] }
325
                                />
326
                                <RichText
327
                                        identifier={ id + "-answer" }
328
                                        className="schema-faq-answer"
329
                                        tagName="p"
330
                                        key={ id + "-answer" }
331
                                        value={ answer }
332
                                        onChange={ this.onChangeAnswer }
333
                                        onFocus={ this.onFocusAnswer }
334
                                        placeholder={ __( "Enter the answer to the question", "wordpress-seo" ) }
335
                                />
336
                                { isSelected &&
×
337
                                        <div className="schema-faq-section-controls-container">
338
                                                { this.getMover() }
339
                                                { this.getButtons() }
340
                                        </div>
341
                                }
342
                        </div>
343
                );
344
        }
345
}
346

347
Question.propTypes = {
×
348
        index: PropTypes.number.isRequired,
349
        attributes: PropTypes.object.isRequired,
350
        onChange: PropTypes.func.isRequired,
351
        insertQuestion: PropTypes.func.isRequired,
352
        removeQuestion: PropTypes.func.isRequired,
353
        onFocus: PropTypes.func.isRequired,
354
        onMoveUp: PropTypes.func.isRequired,
355
        onMoveDown: PropTypes.func.isRequired,
356
        isSelected: PropTypes.bool.isRequired,
357
        isFirst: PropTypes.bool.isRequired,
358
        isLast: PropTypes.bool.isRequired,
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