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

Yoast / wordpress-seo / 2933c98962a853be2048beeb812f0fac9b2a8197

15 Jan 2026 04:12PM UTC coverage: 53.19% (+0.4%) from 52.832%
2933c98962a853be2048beeb812f0fac9b2a8197

Pull #22887

github

web-flow
Merge edcd36026 into d394743dd
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/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
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 {wp.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
         * Handle the focus event on the question editor.
62
         *
63
         * @returns {void}
64
         */
65
        onFocusQuestion() {
66
                this.props.onFocus( "question", this.props.index );
×
67
        }
68

69
        /**
70
         * Handle 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 on 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 on 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
         * Handle 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
         * Handle 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
         * The insert and remove question buttons.
175
         *
176
         * @returns {Component} 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 {Component} 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 imageHtml = `<img class="wp-image-${ media.id }" alt="${ media.alt || "" }" src="${ media.url }" style="max-width: 100%;" />`;
×
246

247
                // Append to existing text (which is now a string).
NEW
248
                const newAnswer = ( answer || "" ) + imageHtml;
×
249

250
                this.props.onChange( question, newAnswer, question, answer, index );
×
251
        }
252

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

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

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

305
                const {
306
                        id,
307
                        question,
308
                        answer,
309
                } = attributes;
×
310

311
                return (
×
312
                        <div className="schema-faq-section" key={ id }>
313
                                <RichText
314
                                        identifier={ id + "-question" }
315
                                        className="schema-faq-question"
316
                                        tagName="p"
317
                                        key={ id + "-question" }
318
                                        value={ question }
319
                                        onChange={ this.onChangeQuestion }
320
                                        onFocus={ this.onFocusQuestion }
321
                                        // The unstableOnFocus prop is added for backwards compatibility with Gutenberg versions <= 15.1 (WordPress 6.2).
322
                                        unstableOnFocus={ 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
                                        // The unstableOnFocus prop is added for backwards compatibility with Gutenberg versions <= 15.1 (WordPress 6.2).
335
                                        unstableOnFocus={ this.onFocusAnswer }
336
                                        placeholder={ __( "Enter the answer to the question", "wordpress-seo" ) }
337
                                />
338
                                { isSelected &&
×
339
                                        <div className="schema-faq-section-controls-container">
340
                                                { this.getMover() }
341
                                                { this.getButtons() }
342
                                        </div>
343
                                }
344
                        </div>
345
                );
346
        }
347
}
348

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