• 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/how-to/components/HowTo.js
1
/* External dependencies */
2
import PropTypes from "prop-types";
3
import styled from "styled-components";
4
import { __ } from "@wordpress/i18n";
5
import { speak } from "@wordpress/a11y";
6
import { get, toString } from "lodash";
7

8
/* Internal dependencies */
9
import HowToStep from "./HowToStep";
10
import buildDurationString from "../utils/buildDurationString";
11
import appendSpace from "../../../components/higherorder/appendSpace";
12

13
import { RichText, InspectorControls } from "@wordpress/block-editor";
14
import { Button, PanelBody, TextControl, ToggleControl } from "@wordpress/components";
15
import { Component, createRef } from "@wordpress/element";
16
import { getImageElements, getImageSrc } from "../../shared-utils";
17

18

19
const RichTextWithAppendedSpace = appendSpace( RichText.Content );
×
20

21
/**
22
 * Modified Text Control to provide a better layout experience.
23
 *
24
 * @returns {wp.Element} The TextControl with additional spacing below.
25
 */
26
const SpacedTextControl = styled( TextControl )`
×
27
        &&& {
28
                margin-bottom: 32px;
29
        }
30
`;
31

32
/**
33
 * A How-to block component.
34
 */
35
export default class HowTo extends Component {
36
        /**
37
         * Constructs a HowTo editor component.
38
         *
39
         * @param {Object} props This component's properties.
40
         *
41
         * @returns {void}
42
         */
43
        constructor( props ) {
44
                super( props );
×
45

46
                this.state = { focus: "" };
×
47

48
                this.changeStep           = this.changeStep.bind( this );
×
49
                this.insertStep           = this.insertStep.bind( this );
×
50
                this.removeStep           = this.removeStep.bind( this );
×
51
                this.swapSteps            = this.swapSteps.bind( this );
×
52
                this.setFocus             = this.setFocus.bind( this );
×
53
                this.addCSSClasses        = this.addCSSClasses.bind( this );
×
54
                this.getListTypeHelp      = this.getListTypeHelp.bind( this );
×
55
                this.toggleListType       = this.toggleListType.bind( this );
×
56
                this.setDurationText      = this.setDurationText.bind( this );
×
57
                this.setFocusToStep       = this.setFocusToStep.bind( this );
×
58
                this.moveStepUp           = this.moveStepUp.bind( this );
×
59
                this.moveStepDown         = this.moveStepDown.bind( this );
×
60
                this.focusDescription     = this.focusDescription.bind( this );
×
61
                this.addDuration          = this.addDuration.bind( this );
×
62
                this.removeDuration       = this.removeDuration.bind( this );
×
63
                this.onChangeDescription  = this.onChangeDescription.bind( this );
×
64
                this.onChangeDays         = this.onChangeDays.bind( this );
×
65
                this.onChangeHours        = this.onChangeHours.bind( this );
×
66
                this.onChangeMinutes      = this.onChangeMinutes.bind( this );
×
67
                this.onAddStepButtonClick = this.onAddStepButtonClick.bind( this );
×
68
                this.daysInput            = createRef();
×
69
                this.addDurationButton    = createRef();
×
70

71
                const defaultDurationText = this.getDefaultDurationText();
×
72
                this.setDefaultDurationText( defaultDurationText );
×
73
        }
74

75
        /**
76
         * Returns the default duration text.
77
         *
78
         * @returns {string} The default duration text.
79
         */
80
        getDefaultDurationText() {
81
                const applyFilters = get( window, "wp.hooks.applyFilters" );
×
82
                let defaultDurationText = __( "Time needed:", "wordpress-seo" );
×
83

84
                if ( applyFilters ) {
×
85
                        defaultDurationText = applyFilters( "wpseo_duration_text", defaultDurationText );
×
86
                }
87

88
                return defaultDurationText;
×
89
        }
90

91
        /**
92
         * Sets the duration text to describe the time the guide in the how-to block takes.
93
         *
94
         * @param {string} text The text to describe the duration.
95
         *
96
         * @returns {void}
97
         */
98
        setDurationText( text ) {
99
                this.props.setAttributes( { durationText: text } );
×
100
        }
101

102
        /**
103
         * Sets the default duration text to describe the time the instructions in the how-to block take, when no duration text
104
         * was provided.
105
         *
106
         * @param {string} text The text to describe the duration.
107
         *
108
         * @returns {void}
109
         */
110
        setDefaultDurationText( text ) {
111
                this.props.setAttributes( { defaultDurationText: text } );
×
112
        }
113

114
        /**
115
         * Handles the Add Step Button click event.
116
         *
117
         * Necessary because insertStep needs to be called without arguments, to ensure the step is added properly.
118
         *
119
         * @returns {void}
120
         */
121
        onAddStepButtonClick() {
NEW
122
                this.insertStep( null, "", "", [], false );
×
123
        }
124

125
        /**
126
         * Generates a pseudo-unique id.
127
         *
128
         * @param {string} [prefix] The prefix to use.
129
         *
130
         * @returns {string} A pseudo-unique string, consisting of the optional prefix + the current time in milliseconds.
131
         */
132
        static generateId( prefix ) {
133
                return `${ prefix }-${ new Date().getTime() }`;
×
134
        }
135

136
        /**
137
         * Replaces the How-to step with the given index.
138
         *
139
         * @param {string}  newName      The new step-name.
140
         * @param {string}  newText      The new step-text.
141
         * @param {string}  previousName The previous step-name.
142
         * @param {string}  previousText The previous step-text.
143
         * @param {number} index        The index of the step that needs to be changed.
144
         *
145
         * @returns {void}
146
         */
147
        changeStep( newName, newText, previousName, previousText, index ) {
148
                const steps = this.props.attributes.steps ? this.props.attributes.steps.slice() : [];
×
149

150
                // If the index exceeds the number of steps, don't change anything.
151
                if ( index >= steps.length ) {
×
152
                        return;
×
153
                }
154

155
                /*
156
                 * Because the DOM re-uses input elements, the changeStep function was triggered when removing/inserting/swapping
157
                 * input elements. We need to check for such events, and return early if the changeStep was called without any
158
                 * user changes to the input field, but because the underlying input elements moved around in the DOM.
159
                 *
160
                 * In essence, when the name at the current index does not match the name that was in the input field previously,
161
                 * the changeStep was triggered by input fields moving in the DOM.
162
                 */
163
                if ( steps[ index ].name !== previousName || steps[ index ].text !== previousText ) {
×
164
                        return;
×
165
                }
166

167
                // Rebuild the step with the newly made changes.
168
                steps[ index ] = {
×
169
                        id: steps[ index ].id,
170
                        name: newName,
171
                        text: newText,
172
                        jsonName: newName,
173
                        jsonText: newText,
174
                };
175

NEW
176
                steps[ index ].images = getImageElements( newText );
×
177

NEW
178
                const imageSrc = getImageSrc( newText );
×
179

180
                if ( imageSrc ) {
×
181
                        steps[ index ].jsonImageSrc = imageSrc;
×
182
                }
183

184
                this.props.setAttributes( { steps } );
×
185
        }
186

187
        /**
188
         * Inserts an empty Step into a how-to block at the given index.
189
         *
190
         * @param {number}  [index]      The index of the Step after which a new Step should be added.
191
         * @param {string}  [name]       The title of the new Step.
192
         * @param {string}  [text]       The description of the new Step.
193
         * @param {array}  [images]      The images of the new Step.
194
         * @param {boolean}    [focus=true] Whether to focus the new Step.
195
         *
196
         * @returns {void}
197
         */
198
        insertStep( index = null, name = "", text = "", images = [], focus = true ) {
×
199
                const steps = this.props.attributes.steps ? this.props.attributes.steps.slice() : [];
×
200

201
                if ( index === null ) {
×
202
                        index = steps.length - 1;
×
203
                }
204

205
                steps.splice( index + 1, 0, {
×
206
                        id: HowTo.generateId( "how-to-step" ),
207
                        name,
208
                        text,
209
                        images,
210
                        jsonName: "",
211
                        jsonText: "",
212
                } );
213

214
                this.props.setAttributes( { steps } );
×
215

216
                if ( focus ) {
×
217
                        setTimeout( this.setFocus.bind( this, `${ index + 1 }:name` ) );
×
218
                        // When moving focus to a newly created step, return and don't use the speak() message.
219
                        return;
×
220
                }
221

222
                speak( __( "New step added", "wordpress-seo" ) );
×
223
        }
224

225
        /**
226
         * Swaps two steps in the how-to block.
227
         *
228
         * @param {number} index1 The index of the first block.
229
         * @param {number} index2 The index of the second block.
230
         *
231
         * @returns {void}
232
         */
233
        swapSteps( index1, index2 ) {
234
                const steps = this.props.attributes.steps ? this.props.attributes.steps.slice() : [];
×
235
                const step  = steps[ index1 ];
×
236

237
                steps[ index1 ] = steps[ index2 ];
×
238
                steps[ index2 ] = step;
×
239

240
                this.props.setAttributes( { steps } );
×
241

242
                const [ focusIndex, subElement ] = this.state.focus.split( ":" );
×
243
                if ( focusIndex === `${ index1 }` ) {
×
244
                        this.setFocus( `${ index2 }:${ subElement }` );
×
245
                }
246

247
                if ( focusIndex === `${ index2 }` ) {
×
248
                        this.setFocus( `${ index1 }:${ subElement }` );
×
249
                }
250
        }
251

252
        /**
253
         * Removes a step from a how-to block.
254
         *
255
         * @param {number} index The index of the step that needs to be removed.
256
         *
257
         * @returns {void}
258
         */
259
        removeStep( index ) {
260
                const steps = this.props.attributes.steps ? this.props.attributes.steps.slice() : [];
×
261

262
                steps.splice( index, 1 );
×
263
                this.props.setAttributes( { steps } );
×
264

265
                let fieldToFocus = "description";
×
266
                if ( steps[ index ] ) {
×
267
                        fieldToFocus = `${ index }:name`;
×
268
                } else if ( steps[ index - 1 ] ) {
×
269
                        fieldToFocus = `${ index - 1 }:text`;
×
270
                }
271

272
                this.setFocus( fieldToFocus );
×
273
        }
274

275
        /**
276
         * Sets the focus to a specific step in the How-to block.
277
         *
278
         * @param {number|string} elementToFocus The element to focus, either the index of the step that should be in focus or name of the input.
279
         *
280
         * @returns {void}
281
         */
282
        setFocus( elementToFocus ) {
283
                if ( elementToFocus === this.state.focus ) {
×
284
                        return;
×
285
                }
286

287
                this.setState( { focus: elementToFocus } );
×
288
        }
289

290
        /**
291
         * Sets the focus to an element within teh specified step.
292
         *
293
         * @param {number} stepIndex      Index of the step to focus.
294
         * @param {string} elementToFocus Name of the element to focus.
295
         *
296
         * @returns {void}
297
         */
298
        setFocusToStep( stepIndex, elementToFocus ) {
299
                this.setFocus( `${ stepIndex }:${ elementToFocus }` );
×
300
        }
301

302
        /**
303
         * Move the step at the specified index one step up.
304
         *
305
         * @param {number} stepIndex Index of the step that should be moved.
306
         *
307
         * @returns {void}
308
         */
309
        moveStepUp( stepIndex ) {
310
                this.swapSteps( stepIndex, stepIndex - 1 );
×
311
        }
312

313
        /**
314
         * Move the step at the specified index one step down.
315
         *
316
         * @param {number} stepIndex Index of the step that should be moved.
317
         *
318
         * @returns {void}
319
         */
320
        moveStepDown( stepIndex ) {
321
                this.swapSteps( stepIndex, stepIndex + 1 );
×
322
        }
323

324
        /**
325
         * Returns an array of How-to step components to be rendered on screen.
326
         *
327
         * @returns {Component[]} The step components.
328
         */
329
        getSteps() {
330
                if ( ! this.props.attributes.steps ) {
×
331
                        return null;
×
332
                }
333

334
                const [ focusIndex ] = this.state.focus.split( ":" );
×
335

336
                return this.props.attributes.steps.map( ( step, index ) => {
×
337
                        return (
×
338
                                <HowToStep
339
                                        key={ step.id }
340
                                        step={ step }
341
                                        index={ index }
342
                                        onChange={ this.changeStep }
343
                                        insertStep={ this.insertStep }
344
                                        removeStep={ this.removeStep }
345
                                        onFocus={ this.setFocusToStep }
346
                                        onMoveUp={ this.moveStepUp }
347
                                        onMoveDown={ this.moveStepDown }
348
                                        isFirst={ index === 0 }
349
                                        isLast={ index === this.props.attributes.steps.length - 1 }
350
                                        isSelected={ focusIndex === `${ index }` }
351
                                        isUnorderedList={ this.props.attributes.unorderedList }
352
                                />
353
                        );
354
                } );
355
        }
356

357
        /**
358
         * Formats the time in the input fields by removing leading zeros.
359
         *
360
         * @param {number} duration    The duration as entered by the user.
361
         * @param {number} maxDuration Optional. The max duration a field can have.
362
         *
363
         * @returns {number} The formatted duration.
364
         */
365
        formatDuration( duration, maxDuration = null ) {
×
366
                if ( duration === "" ) {
×
367
                        return "";
×
368
                }
369

370
                const newDuration = duration.replace( /^[0]+/, "" );
×
371
                if ( newDuration === "" ) {
×
372
                        return 0;
×
373
                }
374

375
                if ( maxDuration !== null ) {
×
376
                        return Math.min( Math.max( 0, parseInt( newDuration, 10 ) ), maxDuration );
×
377
                }
378

379
                return Math.max( 0, parseInt( newDuration, 10 ) );
×
380
        }
381

382
        /**
383
         * Renders the how-to steps.
384
         *
385
         * @param {array} steps The steps data.
386
         *
387
         * @returns {array} The HowToStep elements.
388
         */
389
        static getStepsContent( steps ) {
390
                if ( ! steps ) {
×
391
                        return null;
×
392
                }
393

394
                return steps.map( step => (
×
395
                        <HowToStep.Content
×
396
                                { ...step }
397
                                key={ step.id }
398
                        />
399
                ) );
400
        }
401

402
        /**
403
         * Returns the component to be used to render
404
         * the How-to block on WordPress (e.g. not in the editor).
405
         *
406
         * @param {object} props the attributes of the How-to block.
407
         *
408
         * @returns {Component} The component representing a How-to block.
409
         */
410
        static Content( props ) {
411
                const {
412
                        steps,
413
                        hasDuration,
414
                        days,
415
                        hours,
416
                        minutes,
417
                        description,
418
                        unorderedList,
419
                        additionalListCssClasses,
420
                        className,
421
                        durationText,
422
                        defaultDurationText,
423
                } = props;
×
424

425
                const classNames       = [ "schema-how-to", className ].filter( ( item ) => item ).join( " " );
×
426
                const listClassNames   = [ "schema-how-to-steps", additionalListCssClasses ].filter( ( item ) => item ).join( " " );
×
427

428
                const timeString = buildDurationString( { days, hours, minutes } );
×
429

430
                return (
×
431
                        <div className={ classNames }>
432
                                { ( hasDuration && typeof timeString === "string" && timeString.length > 0 ) &&
×
433
                                        <p className="schema-how-to-total-time">
434
                                                <span className="schema-how-to-duration-time-text">
435
                                                        { durationText || defaultDurationText }
×
436
                                                        &nbsp;
437
                                                </span>
438
                                                { timeString + ". " }
439
                                        </p>
440
                                }
441
                                <RichTextWithAppendedSpace
442
                                        tagName="p"
443
                                        className="schema-how-to-description"
444
                                        value={ description }
445
                                />
446
                                { unorderedList
×
447
                                        ? <ul className={ listClassNames }>{ HowTo.getStepsContent( steps ) }</ul>
448
                                        : <ol className={ listClassNames }>{ HowTo.getStepsContent( steps ) }</ol>
449
                                }
450
                        </div>
451
                );
452
        }
453

454
        /**
455
         * Retrieves a button to add a step at the end of the How-to list.
456
         *
457
         * @returns {Component} The button to add a step.
458
         */
459
        getAddStepButton() {
460
                return (
×
461
                        <Button
462
                                icon="insert"
463
                                onClick={ this.onAddStepButtonClick }
464
                                className="schema-how-to-add-step"
465
                        >
466
                                { __( "Add step", "wordpress-seo" ) }
467
                        </Button>
468
                );
469
        }
470

471
        /**
472
         * Adds CSS classes to this how-to block's list.
473
         *
474
         * @param {string} value The additional css classes.
475
         *
476
         * @returns {void}
477
         */
478
        addCSSClasses( value ) {
479
                this.props.setAttributes( { additionalListCssClasses: value } );
×
480
        }
481

482
        /**
483
         * Toggles the list type of this how-to block.
484
         *
485
         * @param {boolean} checked Whether or not the list is unordered.
486
         *
487
         * @returns {void}
488
         */
489
        toggleListType( checked ) {
490
                this.props.setAttributes( { unorderedList: checked } );
×
491
        }
492

493
        /**
494
         * Returns the help text for this how-to block's list type.
495
         *
496
         * @param {boolean} checked Whether or not the list is unordered.
497
         *
498
         * @returns {string} The list type help string.
499
         */
500
        getListTypeHelp( checked ) {
501
                return checked
×
502
                        ? __( "Showing step items as an unordered list", "wordpress-seo" )
503
                        : __( "Showing step items as an ordered list.", "wordpress-seo" );
504
        }
505

506
        /**
507
         * Set focus to the description field.
508
         *
509
         * @returns {void}
510
         */
511
        focusDescription() {
512
                this.setFocus( "description" );
×
513
        }
514

515
        /**
516
         * Handles the on change event for the how-to description field.
517
         *
518
         * @param {string} value The new description.
519
         *
520
         * @returns {void}
521
         */
522
        onChangeDescription( value ) {
523
                this.props.setAttributes( {
×
524
                        description: value,
525
                        jsonDescription: value,
526
                } );
527
        }
528

529
        /**
530
         * Enables the duration fields and manages focus.
531
         *
532
         * @returns {void}
533
         */
534
        addDuration() {
535
                this.props.setAttributes( { hasDuration: true } );
×
536
                setTimeout( () => this.daysInput.current.focus() );
×
537
        }
538

539
        /**
540
         * Disables the duration fields and manages focus.
541
         *
542
         * @returns {void}
543
         */
544
        removeDuration() {
545
                this.props.setAttributes( { hasDuration: false } );
×
546
                // Wait for the Add Duration button to mount.
547
                setTimeout( () => {
×
548
                        /*
549
                         * Prior to Gutenberg 5.3 the IconButton doesn't support refs. The ref
550
                         * returns the Component instance and attempting to set focus on it
551
                         * triggers a TypeError. To keep it simple, we accept a focus loss.
552
                         * Starting from WordPress 5.2, IconButton does support refs so this
553
                         * check can be removed in the future.
554
                         */
555
                        if ( this.addDurationButton.current instanceof Component ) {
×
556
                                return;
×
557
                        }
558

559
                        this.addDurationButton.current.focus();
×
560
                } );
561
        }
562

563
        /**
564
         * Handles the days input on change event.
565
         *
566
         * @param {SyntheticInputEvent} event The input event.
567
         *
568
         * @returns {void}
569
         */
570
        onChangeDays( event ) {
571
                const newValue = this.formatDuration( event.target.value );
×
572
                this.props.setAttributes( { days: toString( newValue ) } );
×
573
        }
574

575
        /**
576
         * Handles the hours input on change event.
577
         *
578
         * @param {SyntheticInputEvent} event The input event.
579
         *
580
         * @returns {void}
581
         */
582
        onChangeHours( event ) {
583
                const newValue = this.formatDuration( event.target.value, 23 );
×
584
                this.props.setAttributes( { hours: toString( newValue ) } );
×
585
        }
586

587
        /**
588
         * Handles the minutes input on change event.
589
         *
590
         * @param {SyntheticInputEvent} event The input event.
591
         *
592
         * @returns {void}
593
         */
594
        onChangeMinutes( event ) {
595
                const newValue = this.formatDuration( event.target.value, 59 );
×
596
                this.props.setAttributes( { minutes: toString( newValue ) } );
×
597
        }
598

599
        /**
600
         * Returns a component to manage this how-to block's duration.
601
         *
602
         * @returns {Component} The duration editor component.
603
         */
604
        getDuration() {
605
                const { attributes } = this.props;
×
606

607
                if ( ! attributes.hasDuration ) {
×
608
                        return (
×
609
                                <Button
610
                                        onClick={ this.addDuration }
611
                                        className="schema-how-to-duration-button"
612
                                        ref={ this.addDurationButton }
613
                                        icon="insert"
614
                                >
615
                                        { __( "Add total time", "wordpress-seo" ) }
616
                                </Button>
617
                        );
618
                }
619

620
                return (
×
621
                        <fieldset className="schema-how-to-duration">
622
                                <span className="schema-how-to-duration-flex-container" role="presentation">
623
                                        <legend
624
                                                className="schema-how-to-duration-legend"
625
                                        >
626
                                                { attributes.durationText || this.getDefaultDurationText() }
×
627
                                        </legend>
628
                                        <span className="schema-how-to-duration-time-input">
629
                                                <label
630
                                                        htmlFor="schema-how-to-duration-days"
631
                                                        className="screen-reader-text"
632
                                                >
633
                                                        {
634
                                                                /* translators: Hidden accessibility text. */
635
                                                                __( "days", "wordpress-seo" )
636
                                                        }
637
                                                </label>
638
                                                <input
639
                                                        id="schema-how-to-duration-days"
640
                                                        className="schema-how-to-duration-input"
641
                                                        type="number"
642
                                                        value={ attributes.days }
643
                                                        onChange={ this.onChangeDays }
644
                                                        placeholder="DD"
645
                                                        ref={ this.daysInput }
646
                                                />
647
                                                <label
648
                                                        htmlFor="schema-how-to-duration-hours"
649
                                                        className="screen-reader-text"
650
                                                >
651
                                                        { __( "hours", "wordpress-seo" ) }
652
                                                </label>
653
                                                <input
654
                                                        id="schema-how-to-duration-hours"
655
                                                        className="schema-how-to-duration-input"
656
                                                        type="number"
657
                                                        value={ attributes.hours }
658
                                                        onChange={ this.onChangeHours }
659
                                                        placeholder="HH"
660
                                                />
661
                                                <span aria-hidden="true">:</span>
662
                                                <label
663
                                                        htmlFor="schema-how-to-duration-minutes"
664
                                                        className="screen-reader-text"
665
                                                >
666
                                                        { __( "minutes", "wordpress-seo" ) }
667
                                                </label>
668
                                                <input
669
                                                        id="schema-how-to-duration-minutes"
670
                                                        className="schema-how-to-duration-input"
671
                                                        type="number"
672
                                                        value={ attributes.minutes }
673
                                                        onChange={ this.onChangeMinutes }
674
                                                        placeholder="MM"
675
                                                />
676
                                                <Button
677
                                                        className="schema-how-to-duration-delete-button"
678
                                                        icon="trash"
679
                                                        label={ __( "Delete total time", "wordpress-seo" ) }
680
                                                        onClick={ this.removeDuration }
681
                                                />
682
                                        </span>
683
                                </span>
684
                        </fieldset>
685
                );
686
        }
687

688
        /**
689
         * Adds controls to the editor sidebar to control the given parameters.
690
         *
691
         * @param {boolean} unorderedList     Whether to show the list as an unordered list.
692
         * @param {string}  additionalClasses The additional CSS classes to add to the list.
693
         * @param {string}  durationText      The text to describe the duration.
694
         *
695
         * @returns {Component} The controls to add to the sidebar.
696
         */
697
        getSidebar( unorderedList, additionalClasses, durationText ) {
698
                if ( durationText === this.getDefaultDurationText() ) {
×
699
                        durationText = "";
×
700
                }
701

702
                return <InspectorControls>
×
703
                        <PanelBody title={ __( "Settings", "wordpress-seo" ) } className="blocks-font-size">
704
                                <SpacedTextControl
705
                                        label={ __( "CSS class(es) to apply to the steps", "wordpress-seo" ) }
706
                                        value={ additionalClasses }
707
                                        onChange={ this.addCSSClasses }
708
                                        help={ __( "Optional. This can give you better control over the styling of the steps.", "wordpress-seo" ) }
709
                                />
710
                                <SpacedTextControl
711
                                        label={ __( "Describe the duration of the instruction:", "wordpress-seo" ) }
712
                                        value={ durationText }
713
                                        onChange={ this.setDurationText }
714
                                        help={ __( "Optional. Customize how you want to describe the duration of the instruction", "wordpress-seo" ) }
715
                                        placeholder={ this.getDefaultDurationText() }
716
                                />
717
                                <ToggleControl
718
                                        label={ __( "Unordered list", "wordpress-seo" ) }
719
                                        checked={ unorderedList || false }
×
720
                                        onChange={ this.toggleListType }
721
                                        help={ this.getListTypeHelp }
722
                                />
723
                        </PanelBody>
724
                </InspectorControls>;
725
        }
726

727
        /**
728
         * Renders this component.
729
         *
730
         * @returns {Component} The how-to block editor.
731
         */
732
        render() {
733
                const { attributes, className } = this.props;
×
734

735
                const classNames     = [ "schema-how-to", className ].filter( ( item ) => item ).join( " " );
×
736
                const listClassNames = [ "schema-how-to-steps", attributes.additionalListCssClasses ].filter( ( item ) => item ).join( " " );
×
737

738
                return (
×
739
                        <div className={ classNames }>
740
                                { this.getDuration() }
741
                                <RichText
742
                                        identifier="description"
743
                                        tagName="p"
744
                                        className="schema-how-to-description"
745
                                        value={ attributes.description }
746
                                        onChange={ this.onChangeDescription }
747
                                        onFocus={ this.focusDescription }
748
                                        // The unstableOnFocus prop is added for backwards compatibility with Gutenberg versions <= 15.1 (WordPress 6.2).
749
                                        unstableOnFocus={ this.focusDescription }
750
                                        placeholder={ __( "Enter a description", "wordpress-seo" ) }
751
                                />
752
                                <ul className={ listClassNames }>
753
                                        { this.getSteps() }
754
                                </ul>
755
                                <div className="schema-how-to-buttons">{ this.getAddStepButton() }</div>
756
                                { this.getSidebar( attributes.unorderedList, attributes.additionalListCssClasses, attributes.durationText ) }
757
                        </div>
758
                );
759
        }
760
}
761

762
HowTo.propTypes = {
×
763
        attributes: PropTypes.object.isRequired,
764
        setAttributes: PropTypes.func.isRequired,
765
        className: PropTypes.string,
766
};
767

768
HowTo.defaultProps = {
×
769
        className: "",
770
};
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