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

Yoast / wordpress-seo / 466662439254d4f5c9aace15e9920072b12a1900

01 Aug 2024 11:07AM CUT coverage: 48.697% (-5.4%) from 54.073%
466662439254d4f5c9aace15e9920072b12a1900

push

github

web-flow
Merge pull request #21511 from Yoast/fix/no-available-content

Allows setting a reason for a disabled Yoast AI Optimize button

7460 of 13482 branches covered (55.33%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 2 files covered. (100.0%)

4425 existing lines in 144 files now uncovered.

25255 of 53699 relevant lines covered (47.03%)

42524.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.67
/packages/js/src/ai-assessment-fixes/components/ai-assessment-fixes-button.js
1
import PropTypes from "prop-types";
2
import { __ } from "@wordpress/i18n";
3
import { useCallback, useRef, useState } from "@wordpress/element";
4
import { doAction } from "@wordpress/hooks";
5
import { useSelect, useDispatch } from "@wordpress/data";
6

7
/* Yoast dependencies */
8
import { IconAIFixesButton, SparklesIcon } from "@yoast/components";
9
import { Modal, useToggleState } from "@yoast/ui-library";
10
import { Paper } from "yoastseo";
11

12
/* Internal dependencies */
13
import { ModalContent } from "./modal-content";
14
import { getAllBlocks } from "../../helpers/getAllBlocks";
15
import { ReactComponent as LockClosed } from "../../../images/lock-closed.svg";
16

17
/**
18
 * The AI Assessment Fixes button component.
19
 *
20
 * @param {string} id The assessment ID for which the AI fixes should be applied to.
21
 * @param {boolean} isPremium Whether the premium add-on is active.
22
 *
23
 * @returns {JSX.Element} The AI Assessment Fixes button.
24
 */
25
const AIAssessmentFixesButton = ( { id, isPremium } ) => {
2✔
26
        const aiFixesId = id + "AIFixes";
16✔
27
        const [ isModalOpen, , , setIsModalOpenTrue, setIsModalOpenFalse ] = useToggleState( false );
16✔
28
        const activeAIButtonId = useSelect( select => select( "yoast-seo/editor" ).getActiveAIFixesButton(), [] );
16✔
29
        const activeMarker = useSelect( select => select( "yoast-seo/editor" ).getActiveMarker(), [] );
16✔
30
        const { setActiveAIFixesButton, setActiveMarker, setMarkerPauseStatus } = useDispatch( "yoast-seo/editor" );
16✔
31
        const focusElementRef = useRef( null );
16✔
32
        const [ buttonClass, setButtonClass ] = useState( "" );
16✔
33

34
        const defaultLabel = __( "Optimize with AI", "wordpress-seo" );
16✔
35
        const htmlLabel = __( "Please switch to the visual editor to optimize with AI.", "wordpress-seo" );
16✔
36

37
        // The button is pressed when the active AI button id is the same as the current button id.
38
        const isButtonPressed = activeAIButtonId === aiFixesId;
16✔
39

40
        // Enable the button when:
41
        // (1) other AI buttons are not pressed.
42
        // (2) the AI button is not disabled.
43
        // (3) the editor is in visual mode.
44
        // (4) all blocks are in visual mode.
45
        const { isEnabled, ariaLabel } = useSelect( ( select ) => {
16✔
46
                if ( activeAIButtonId !== null && ! isButtonPressed ) {
16!
UNCOV
47
                        return {
×
48
                                isEnabled: false,
49
                                ariaLabel: null,
50
                        };
51
                }
52

53
                const disabledAIButtons = select( "yoast-seo/editor" ).getDisabledAIFixesButtons();
16✔
54
                if ( Object.keys( disabledAIButtons ).includes( aiFixesId ) ) {
16✔
55
                        return {
2✔
56
                                isEnabled: false,
57
                                ariaLabel: disabledAIButtons[ aiFixesId ],
58
                        };
59
                }
60

61
                const editorMode = select( "core/edit-post" ).getEditorMode();
14✔
62
                if ( editorMode !== "visual" ) {
14✔
63
                        return {
2✔
64
                                isEnabled: false,
65
                                ariaLabel: htmlLabel,
66
                        };
67
                }
68

69
                const blocks = getAllBlocks( select( "core/block-editor" ).getBlocks() );
12✔
70
                const allVisual = blocks.every( block => select( "core/block-editor" ).getBlockMode( block.clientId ) === "visual" );
12✔
71
                return {
12✔
72
                        isEnabled: allVisual,
73
                        ariaLabel: allVisual ? defaultLabel : htmlLabel,
6✔
74
                };
75
        }, [ isButtonPressed, activeAIButtonId ] );
76

77
        /**
78
         * Handles the button press state.
79
         * @returns {void}
80
         */
81
        const handlePressedButton = () => {
16✔
82
                // If there is an active marker when the AI fixes button is clicked, remove it.
83
                if ( activeMarker ) {
×
84
                        setActiveMarker( null );
×
85
                        setMarkerPauseStatus( false );
×
86
                        // Remove highlighting from the editor.
87
                        window.YoastSEO.analysis.applyMarks( new Paper( "", {} ), [] );
×
88
                }
89

90
                /* If the current pressed button ID is the same as the active AI button id,
91
                we want to set the active AI button to null. Otherwise, update the active AI button ID. */
92
                if ( aiFixesId === activeAIButtonId ) {
×
93
                        setActiveAIFixesButton( null );
×
94
                } else {
95
                        setActiveAIFixesButton( aiFixesId );
×
96
                }
97

98
                // Dismiss the tooltip when the button is pressed.
99
                setButtonClass( "" );
×
100
        };
101

102
        const handleClick = useCallback( () => {
16✔
103
                if ( isPremium ) {
×
104
                        doAction( "yoast.ai.fixAssessments", aiFixesId );
×
105
                        /* Only handle the pressed button state in Premium.
106
                        We don't want to change the background color of the button and other styling when it's pressed in Free.
107
                        This is because clicking on the button in Free will open the modal, and the button will not be in a pressed state. */
108
                        handlePressedButton();
×
109
                } else {
110
                        setIsModalOpenTrue();
×
111
                }
112
        }, [ handlePressedButton, setIsModalOpenTrue ] );
113

114
        // Add tooltip classes on mouse enter and remove them on mouse leave.
115
        const handleMouseEnter = useCallback( () => {
16✔
116
                if ( ariaLabel ) {
×
117
                        const direction = isEnabled ? "yoast-tooltip-w" : "yoast-tooltip-nw";
×
118
                        setButtonClass( `yoast-tooltip yoast-tooltip-multiline ${ direction }` );
×
119
                }
120
        }, [ isEnabled, ariaLabel ] );
121

122
        const handleMouseLeave = useCallback( () => {
16✔
123
                // Remove tooltip classes on mouse leave
124
                setButtonClass( "" );
×
125
        }, [] );
126

127
        return (
16✔
128
                <IconAIFixesButton
129
                        onClick={ handleClick }
130
                        ariaLabel={ ariaLabel }
131
                        onPointerEnter={ handleMouseEnter }
132
                        onPointerLeave={ handleMouseLeave }
133
                        id={ aiFixesId }
134
                        className={ `ai-button ${buttonClass}` }
135
                        pressed={ isButtonPressed }
136
                        disabled={ ! isEnabled }
137
                >
138
                        { ! isPremium && <LockClosed className="yst-fixes-button__lock-icon" /> }
10✔
139
                        <SparklesIcon pressed={ isButtonPressed } />
140
                        {
141
                                // We put the logic for the Upsell component in place.
142
                                // The Modal below is only a placeholder/mock. When we have the design for the real upsell, the modal should be replaced.
143
                                isModalOpen && <Modal className="yst-introduction-modal" isOpen={ isModalOpen } onClose={ setIsModalOpenFalse } initialFocus={ focusElementRef }>
8!
144
                                        <Modal.Panel className="yst-max-w-lg yst-p-0 yst-rounded-3xl yst-introduction-modal-panel">
145
                                                <ModalContent onClose={ setIsModalOpenFalse } focusElementRef={ focusElementRef } />
146
                                        </Modal.Panel>
147
                                </Modal>
148
                        }
149
                </IconAIFixesButton>
150
        );
151
};
152

153
AIAssessmentFixesButton.propTypes = {
2✔
154
        id: PropTypes.string.isRequired,
155
        isPremium: PropTypes.bool,
156
};
157

158
AIAssessmentFixesButton.defaultProps = {
2✔
159
        isPremium: false,
160
};
161

162
export default AIAssessmentFixesButton;
163

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

© 2025 Coveralls, Inc