Skip to content

Commit

Permalink
Merge pull request #21984 from Yoast/415-add-the-consent-modal-to-sit…
Browse files Browse the repository at this point in the history
…e-kit-setup-flow

Add the Site Kit consent modal
  • Loading branch information
thijsoo authored Jan 29, 2025
2 parents 50c07b2 + 8f50e14 commit 1a8549b
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 27 deletions.
78 changes: 67 additions & 11 deletions packages/js/src/shared-admin/components/site-kit-consent-modal.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,88 @@
import { Modal } from "@yoast/ui-library";
import { ArrowSmRightIcon } from "@heroicons/react/solid";
import { __ } from "@wordpress/i18n";
import { PropTypes } from "prop-types";
import { Button, Modal, useSvgAria } from "@yoast/ui-library";
import PropTypes from "prop-types";
import { OutboundLink } from "./outbound-link";

/**
* The Site Kit consent modal component.
*
* @param {boolean} isOpen Whether the modal is open.
* @param {Function} onClose Callback to close the modal.
* @param {function} onClose Callback to close the modal.
* @param {function} onGrantConsent Callback to grant consent.
* @param {string} learnMoreLink The learn more link.
* @param {string} imageLink The image link.
*
* @returns {JSX.Element} The Site Kit consent modal component.
*/
export const SiteKitConsentModal = ( { isOpen, onClose } ) => {
export const SiteKitConsentModal = ( {
isOpen,
onClose,
onGrantConsent,
learnMoreLink = "http://yoa.st/sitekit-consent-learn-more",
imageLink = "",
} ) => {
const svgAriaProps = useSvgAria();

return (
<Modal
isOpen={ isOpen }
onClose={ onClose }
>
<Modal.Panel>
<Modal.Title>{ __( "Connect Site Kit by Google", "wordpress-seo" ) }</Modal.Title>
<Modal.Description>
{ __( "Connect your Google account to view traffic and search rankings on your dashboard.", "wordpress-seo" ) }
</Modal.Description>
<Modal.Panel className="yst-max-w-lg yst-p-0 yst-rounded-3xl" hasCloseButton={ false }>
<Modal.CloseButton
className="yst-bg-transparent yst-text-gray-500 focus:yst-ring-offset-0"
onClick={ onClose }
screenReaderText={ __( "Close", "wordpress-seo" ) }
/>
<div className="yst-px-10 yst-pt-10 yst-bg-gradient-to-b yst-from-primary-500/25 yst-to-[80%] yst-text-center">
<img
className="yst-rounded-md yst-drop-shadow-md yst-bg-slate-100 yst-aspect-video"
alt=""
loading="lazy"
decoding="async"
src={ imageLink }
/>
</div>
<div className="yst-px-10 yst-pb-4 yst-flex yst-flex-col yst-items-center">
<div className="yst-mt-4 yst-mx-1.5 yst-text-center">
<h3 className="yst-text-slate-900 yst-text-lg yst-font-medium">
{ __( "Grant consent to connect with Site Kit by Google", "wordpress-seo" ) }
</h3>
<div className="yst-mt-2 yst-text-slate-600 yst-text-sm">
{ __( "Give us permission to access your Site Kit data, allowing insights from tools like Google Analytics and Search Console to be displayed directly on your dashboard.", "wordpress-seo" ) }
{ " " }
<OutboundLink
className="yst-no-underline yst-font-medium"
variant="primary"
href={ learnMoreLink }
>
{ __( "Learn more", "wordpress-seo" ) }
<ArrowSmRightIcon
className="yst-inline yst-h-4 yst-w-4 yst-ms-1 rtl:yst-rotate-180"
{ ...svgAriaProps }
/>
</OutboundLink>
</div>
</div>
<div className="yst-w-full yst-flex yst-mt-10">
<Button className="yst-grow" size="extra-large" variant="primary" onClick={ onGrantConsent || onClose }>
{ __( "Grant consent", "wordpress-seo" ) }
</Button>
</div>
<Button as="a" className="yst-mt-4" variant="tertiary" onClick={ onClose }>
{ __( "Close", "wordpress-seo" ) }
</Button>
</div>
</Modal.Panel>
</Modal>
);
};

SiteKitConsentModal.propTypes = {
isOpen: PropTypes.bool,
onClose: PropTypes.func,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onGrantConsent: PropTypes.func,
learnMoreLink: PropTypes.string,
imageLink: PropTypes.string,
};
58 changes: 45 additions & 13 deletions packages/ui-library/src/components/modal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,49 @@ Title.defaultProps = {
as: "h1",
};

/**
* @param {string} [className] Additional classname for the button.
* @param {function} [onClick] Function that is called when the user clicks the button. Defaults to the onClose function from the context.
* @param {string} [screenReaderText] The screen reader text. Used when no children are provided.
* @param {JSX.node} [children] Possible to override the default screen reader text and X icon.
* @returns {JSX.Element} The close button.
*/
const CloseButton = forwardRef( ( { className, onClick, screenReaderText, children, ...props }, ref ) => {
const { onClose } = useModalContext();
const svgAriaProps = useSvgAria();

return (
<div className="yst-modal__close">
<button
ref={ ref }
type="button"
onClick={ onClick || onClose }
className={ classNames( "yst-modal__close-button", className ) }
{ ...props }
>
{ children || <>
<span className="yst-sr-only">{ screenReaderText }</span>
<XIcon className="yst-h-6 yst-w-6" { ...svgAriaProps } />
</> }
</button>
</div>
);
} );

CloseButton.displayName = "Modal.CloseButton";
CloseButton.propTypes = {
className: PropTypes.string,
onClick: PropTypes.func.isRequired,
screenReaderText: PropTypes.string,
children: PropTypes.node,
};
CloseButton.defaultProps = {
className: "",
screenReaderText: "Close",
// eslint-disable-next-line no-undefined
children: undefined,
};

/**
* @param {JSX.node} children Contents of the modal.
* @param {string} [className] Additional class names.
Expand All @@ -55,25 +98,13 @@ Title.defaultProps = {
* @returns {JSX.Element} The panel.
*/
const Panel = forwardRef( ( { children, className = "", hasCloseButton = true, closeButtonScreenReaderText = "Close", ...props }, ref ) => {
const { onClose } = useModalContext();
const svgAriaProps = useSvgAria();

return (
<Dialog.Panel
ref={ ref }
className={ classNames( "yst-modal__panel", className ) }
{ ...props }
>
{ hasCloseButton && <div className="yst-modal__close">
<button
type="button"
onClick={ onClose }
className="yst-modal__close-button"
>
<span className="yst-sr-only">{ closeButtonScreenReaderText }</span>
<XIcon className="yst-h-6 yst-w-6" { ...svgAriaProps } />
</button>
</div> }
{ hasCloseButton && <CloseButton screenReaderText={ closeButtonScreenReaderText } /> }
{ children }
</Dialog.Panel>
);
Expand Down Expand Up @@ -178,6 +209,7 @@ Modal.defaultProps = {

Modal.Panel = Panel;
Modal.Title = Title;
Modal.CloseButton = CloseButton;
Modal.Description = Dialog.Description;
Modal.Description.displayName = "Modal.Description";
Modal.Container = Container;
Expand Down
68 changes: 65 additions & 3 deletions packages/ui-library/src/components/modal/stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import Modal, { classNameMap } from ".";
import { InteractiveDocsPage } from "../../../.storybook/interactive-docs-page";
import Button from "../../elements/button";
import TextInput from "../../elements/text-input";
import { useModalContext } from "./hooks";

const Template = ( { isOpen: initialIsOpen, onClose: _, children, ...props } ) => {
const Template = ( { isOpen: initialIsOpen, onClose: _, children, panelProps, ...props } ) => {
const [ isOpen, setIsOpen ] = useState( initialIsOpen );
const openModal = useCallback( () => setIsOpen( true ), [] );
const closeModal = useCallback( () => setIsOpen( false ), [] );
Expand All @@ -15,7 +16,7 @@ const Template = ( { isOpen: initialIsOpen, onClose: _, children, ...props } ) =
<>
<Button onClick={ openModal }>Open modal</Button>
<Modal { ...props } isOpen={ isOpen } onClose={ closeModal }>
<Modal.Panel>
<Modal.Panel { ...panelProps }>
{ children }
</Modal.Panel>
</Modal>
Expand All @@ -27,6 +28,7 @@ Template.propTypes = {
isOpen: PropTypes.bool,
onClose: PropTypes.func,
children: PropTypes.node.isRequired,
panelProps: PropTypes.object,
};

export const Factory = {
Expand All @@ -51,6 +53,57 @@ export const WithPanel = {
},
};

const CustomCloseButton = ( props ) => {
const { onClose } = useModalContext();

return <Button { ...props } onClick={ onClose } />;
};

export const WithPanelAndAdjustedCloseButton = {
name: "With panel and adjusted close button",
parameters: {
controls: { disable: false },
docs: {
description: {
story: "Using the `Modal.Panel` component with an adjusted `Modal.CloseButton`.",
},
},
},
args: {
children: <>
<Modal.CloseButton className="yst-bg-transparent yst-text-gray-500 focus:yst-ring-offset-0" screenReaderText="Close" />
<p>Using the <strong>className</strong> prop to change the styling. Works nicely if the modal has a different background.</p>
</>,
panelProps: {
className: "yst-bg-gradient-to-b yst-from-primary-500/25 yst-to-[80%]",
hasCloseButton: false,
},
},
};

export const WithPanelAndCustomCloseButton = {
name: "With panel and custom close button",
parameters: {
controls: { disable: false },
docs: {
description: {
story: "Using the `Modal.Panel` component with a custom close button. The close button is a separate component and can be styled and positioned as needed.",
},
},
},
args: {
children: <div className="yst-flex yst-flex-col yst-gap-2">
<p>
Below is now the custom close button. It uses the <strong>useModalContext</strong> hook to get the <strong>onClose</strong> function.
</p>
<CustomCloseButton className="yst-w-fit yst-self-center" variant="primary">Close</CustomCloseButton>
</div>,
panelProps: {
hasCloseButton: false,
},
},
};

export const WithTitleAndDescription = {
name: "With title and description",
parameters: {
Expand Down Expand Up @@ -175,7 +228,16 @@ export default {
description: {
component: "An uncontrolled modal component. For the purpose of this story, the `children`, `isOpen` and `onClose` are wrapped. So be aware that in the `Show code`, these are not reflected!",
},
page: () => <InteractiveDocsPage stories={ [ WithPanel, WithTitleAndDescription, WithContainer, InitialFocus ] } />,
page: () => <InteractiveDocsPage
stories={ [
WithPanel,
WithPanelAndAdjustedCloseButton,
WithPanelAndCustomCloseButton,
WithTitleAndDescription,
WithContainer,
InitialFocus,
] }
/>,
},
},
args: {
Expand Down

0 comments on commit 1a8549b

Please sign in to comment.