diff --git a/projects/plugins/jetpack/changelog/add-donations-modal-display b/projects/plugins/jetpack/changelog/add-donations-modal-display
new file mode 100644
index 000000000000..2fdaa7934787
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/add-donations-modal-display
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Donations block: add modal display mode with trigger button, configurable icon, and animated overlay.
diff --git a/projects/plugins/jetpack/extensions/blocks/donations/block.json b/projects/plugins/jetpack/extensions/blocks/donations/block.json
index 2ac0c242e4da..fafdc694a14a 100644
--- a/projects/plugins/jetpack/extensions/blocks/donations/block.json
+++ b/projects/plugins/jetpack/extensions/blocks/donations/block.json
@@ -50,18 +50,6 @@
"fontWeight": true,
"lineHeight": true,
"letterSpacing": true
- },
- "__experimentalBorder": {
- "color": true,
- "radius": true,
- "style": true,
- "width": true,
- "__experimentalDefaultControls": {
- "color": true,
- "radius": true,
- "style": true,
- "width": true
- }
}
},
"attributes": {
@@ -180,6 +168,28 @@
},
"maximumAmount": {
"type": "number"
+ },
+ "displayMode": {
+ "type": "string",
+ "enum": [ "inline", "modal" ],
+ "default": "inline"
+ },
+ "triggerButtonText": {
+ "type": "string"
+ },
+ "triggerIcon": {
+ "type": "string",
+ "default": "heart"
+ },
+ "triggerSticky": {
+ "type": "boolean",
+ "default": false
+ },
+ "blockBorder": {
+ "type": "object"
+ },
+ "blockBorderRadius": {
+ "type": [ "string", "object" ]
}
},
"example": {}
diff --git a/projects/plugins/jetpack/extensions/blocks/donations/build-custom-styles.js b/projects/plugins/jetpack/extensions/blocks/donations/build-custom-styles.js
index a77c4f23d5f8..56b890c1f640 100644
--- a/projects/plugins/jetpack/extensions/blocks/donations/build-custom-styles.js
+++ b/projects/plugins/jetpack/extensions/blocks/donations/build-custom-styles.js
@@ -73,6 +73,9 @@ const buildCustomStyles = ( attributes, scope ) => {
buttonAlignment,
buttonBorderRadius,
contentAlignment,
+ blockBorder,
+ blockBorderRadius,
+ displayMode,
} = attributes;
const rules = [];
@@ -190,6 +193,20 @@ const buildCustomStyles = ( attributes, scope ) => {
}
}
+ if ( displayMode !== 'modal' ) {
+ const wrapperBorderDecls = [
+ ...borderDecls( blockBorder ),
+ ...radiusDecls( blockBorderRadius ),
+ ];
+ if ( wrapperBorderDecls.length ) {
+ rules.push( `.wp-block-jetpack-donations${ scope }{${ wrapperBorderDecls.join( ';' ) }}` );
+ }
+ }
+
+ if ( displayMode === 'modal' && [ 'left', 'center', 'right' ].includes( contentAlignment ) ) {
+ rules.push( `${ scope }{text-align:${ contentAlignment }}` );
+ }
+
return rules.join( '' );
};
diff --git a/projects/plugins/jetpack/extensions/blocks/donations/common.scss b/projects/plugins/jetpack/extensions/blocks/donations/common.scss
index 856c25862832..101b574619dd 100644
--- a/projects/plugins/jetpack/extensions/blocks/donations/common.scss
+++ b/projects/plugins/jetpack/extensions/blocks/donations/common.scss
@@ -110,6 +110,13 @@
margin: 0;
}
+ // Pop-up display mode: suppress the default border — the block wrapper is
+ // larger than just the trigger button, so a border here looks wrong.
+ // User-set borders in In-page mode are handled via the per-instance ' : '',
+ esc_html( $trigger_text ),
+ esc_attr( $modal_id ),
+ $trigger_svg,
+ esc_attr__( 'Close', 'jetpack' ),
+ esc_attr( $tab_content_class )
+ );
+ }
+
return sprintf(
'
%9$s
@@ -314,7 +383,7 @@ function build_security_data_attrs( $attr, $currency ) {
$attrs['data-min-error'] = sprintf(
/* translators: %s: minimum donation amount formatted with currency symbol */
__( 'The minimum donation amount is %s.', 'jetpack' ),
- \Jetpack_Currencies::format_price( $min_amount, $currency )
+ \Jetpack_Currencies::format_price( (string) $min_amount, $currency )
);
}
if ( null !== $max_amount ) {
@@ -322,14 +391,14 @@ function build_security_data_attrs( $attr, $currency ) {
$attrs['data-max-error'] = sprintf(
/* translators: %s: maximum donation amount formatted with currency symbol */
__( 'The maximum donation amount is %s.', 'jetpack' ),
- \Jetpack_Currencies::format_price( $max_amount, $currency )
+ \Jetpack_Currencies::format_price( (string) $max_amount, $currency )
);
}
$stripe_min = \Jetpack_Memberships::SUPPORTED_CURRENCIES[ $currency ] ?? 1;
$attrs['data-stripe-min-error'] = sprintf(
/* translators: %s: payment processor minimum donation amount formatted with currency symbol */
_x( 'The minimum donation amount is %s.', 'payment processor minimum', 'jetpack' ),
- \Jetpack_Currencies::format_price( $stripe_min, $currency )
+ \Jetpack_Currencies::format_price( (string) $stripe_min, $currency )
);
return $attrs;
}
@@ -460,6 +529,26 @@ function build_custom_styles( $attr, $scope ) {
. $scope . ' .donations__donate-button{display:block;width:100%;box-sizing:border-box;text-align:center}';
}
+ // Pop-up display mode: wire up trigger-button alignment via text-align on
+ // the wrapper so the inline-flex button responds to the alignment toolbar.
+ if ( 'modal' === ( $attr['displayMode'] ?? 'inline' ) ) {
+ if ( in_array( $content_alignment, array( 'left', 'center', 'right' ), true ) ) {
+ $rules[] = $scope . '{text-align:' . $content_alignment . '}';
+ }
+ }
+
+ // Wrapper border (In-page mode only). Compound selector (.wp-block-jetpack-donations.jp-donations-N)
+ // has specificity 0,2,0 and wins over the default single-class rule (0,1,0) in common.scss.
+ if ( 'modal' !== ( $attr['displayMode'] ?? 'inline' ) ) {
+ $wrapper_border_decls = array_merge(
+ build_border_decls( $attr['blockBorder'] ?? null ),
+ build_radius_decls( $attr['blockBorderRadius'] ?? null )
+ );
+ if ( $wrapper_border_decls ) {
+ $rules[] = '.wp-block-jetpack-donations' . $scope . '{' . implode( ';', $wrapper_border_decls ) . '}';
+ }
+ }
+
return implode( '', $rules );
}
@@ -569,24 +658,63 @@ function sanitize_css_value( $value ) {
*/
function get_default_texts() {
return array(
- 'chooseAmountText' => __( 'Choose an amount', 'jetpack' ),
- 'customAmountText' => __( 'Or enter a custom amount', 'jetpack' ),
- 'extraText' => __( 'Your contribution is appreciated.', 'jetpack' ),
- 'oneTimeDonation' => array(
+ 'chooseAmountText' => __( 'Choose an amount', 'jetpack' ),
+ 'customAmountText' => __( 'Or enter a custom amount', 'jetpack' ),
+ 'extraText' => __( 'Your contribution is appreciated.', 'jetpack' ),
+ 'triggerButtonText' => __( 'Donate', 'jetpack' ),
+ 'oneTimeDonation' => array(
'heading' => __( 'Make a one-time donation', 'jetpack' ),
'buttonText' => __( 'Donate', 'jetpack' ),
),
- 'monthlyDonation' => array(
+ 'monthlyDonation' => array(
'heading' => __( 'Make a monthly donation', 'jetpack' ),
'buttonText' => __( 'Donate monthly', 'jetpack' ),
),
- 'annualDonation' => array(
+ 'annualDonation' => array(
'heading' => __( 'Make a yearly donation', 'jetpack' ),
'buttonText' => __( 'Donate yearly', 'jetpack' ),
),
);
}
+/**
+ * Return inline SVG markup for a trigger button icon.
+ *
+ * Path data sourced from icons.js ICON_SVG_PATHS — keep in sync.
+ *
+ * @param string $icon_key Icon key (e.g. 'coffee', 'heart', 'gift').
+ * @return string SVG HTML string, or '' when key is 'none' or unknown.
+ */
+function get_trigger_icon_svg( $icon_key ) {
+ $paths = array(
+ 'heart' => array( 'd' => 'M16.5 4.5c2.206 0 4 1.794 4 4 0 4.67-5.543 8.94-8.5 11.023C9.043 17.44 3.5 13.17 3.5 8.5c0-2.206 1.794-4 4-4 1.298 0 2.522.638 3.273 1.706L12 7.953l1.227-1.746c.75-1.07 1.975-1.707 3.273-1.707m0-1.5c-1.862 0-3.505.928-4.5 2.344C11.005 3.928 9.362 3 7.5 3 4.462 3 2 5.462 2 8.5c0 5.72 6.5 10.438 10 12.85 3.5-2.412 10-7.13 10-12.85C22 5.462 19.538 3 16.5 3z' ),
+ 'gift' => array( 'd' => 'M15.333 4C16.6677 4 17.75 5.0823 17.75 6.41699V6.75C17.75 7.20058 17.6394 7.62468 17.4473 8H18.5C19.2767 8 19.9154 8.59028 19.9922 9.34668L20 9.5V18.5C20 19.3284 19.3284 20 18.5 20H5.5C4.72334 20 4.08461 19.4097 4.00781 18.6533L4 18.5V9.5L4.00781 9.34668C4.07949 8.64069 4.64069 8.07949 5.34668 8.00781L5.5 8H6.55273C6.36065 7.62468 6.25 7.20058 6.25 6.75V6.41699C6.25 5.0823 7.3323 4 8.66699 4C10.0436 4.00011 11.2604 4.68183 12 5.72559C12.7396 4.68183 13.9564 4.00011 15.333 4ZM5.5 18.5H11.25V9.5H5.5V18.5ZM12.75 18.5H18.5V9.5H12.75V18.5ZM8.66699 5.5C8.16073 5.5 7.75 5.91073 7.75 6.41699V6.75C7.75 7.44036 8.30964 8 9 8H11.2461C11.2021 6.61198 10.0657 5.50017 8.66699 5.5ZM15.333 5.5C13.9343 5.50017 12.7979 6.61198 12.7539 8H15C15.6904 8 16.25 7.44036 16.25 6.75V6.41699C16.25 5.91073 15.8393 5.5 15.333 5.5Z' ),
+ 'star' => array( 'd' => 'M11.776 4.454a.25.25 0 01.448 0l2.069 4.192a.25.25 0 00.188.137l4.626.672a.25.25 0 01.139.426l-3.348 3.263a.25.25 0 00-.072.222l.79 4.607a.25.25 0 01-.362.263l-4.138-2.175a.25.25 0 00-.232 0l-4.138 2.175a.25.25 0 01-.363-.263l.79-4.607a.25.25 0 00-.071-.222L4.754 9.881a.25.25 0 01.139-.426l4.626-.672a.25.25 0 00.188-.137l2.069-4.192z' ),
+ 'thumbs-up' => array( 'd' => 'm3 12 1 8h1.5l-1-8H3Zm15.8-2h-4.4l.8-3.6c.3-1.3-.7-2.4-1.9-2.4h-.2c-.6 0-1.2.3-1.6.8l-5 6.6c-.3.4-.4.8-.4 1.2v.2l.7 5.4v.2c.2.9 1 1.5 1.9 1.5h8.2c.9 0 1.7-.6 1.9-1.4l1.8-6c.4-1.3-.6-2.6-1.9-2.6Zm.5 2.1-1.8 6c0 .2-.3.4-.5.4H8.8c-.3 0-.5-.2-.5-.4l-.7-5.4v-.4l5-6.6c0-.1.2-.2.4-.2h.2c.3 0 .6.3.5.6l-.8 3.6c-.1.4 0 .9.3 1.3s.7.6 1.2.6h4.4c.3 0 .6.3.5.6Z' ),
+ 'smiley' => array( 'd' => 'M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5 14.67 11 15.5 11zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z' ),
+ 'coffee' => array( 'd' => 'M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.89 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4zM9 1v2M12 1v2M15 1v2' ),
+ 'tip-jar' => array( 'd' => 'M9 3h6c0-1.1-.9-2-2-2H11C9.9 1 9 1.9 9 3zm7 0H8c-.55 0-1 .45-1 1v1.5c0 .55.45 1 1 1h8c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1zm-1 3.5H9V19c0 1.1.9 2 2 2h2c1.1 0 2-.9 2-2V6.5zm-3 1.5h2v1.5l-1 1.5-1-1.5V8z' ),
+ 'hand-heart' => array( 'd' => 'M15.5 2.1c-1.1 0-2 .6-2.5 1.4-.5-.9-1.4-1.4-2.5-1.4C8.8 2.1 7.5 3.4 7.5 5c0 2.5 4.5 5.9 5.5 6.6 1-.7 5.5-4.1 5.5-6.6 0-1.6-1.3-2.9-3-2.9zM9 14H7l-2 7h14l-2-7h-2l-1 3H10l-1-3z' ),
+ 'people' => array(
+ 'd' => 'M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z',
+ 'fill-rule' => 'evenodd',
+ ),
+ );
+
+ if ( ! isset( $paths[ $icon_key ] ) || 'none' === $icon_key ) {
+ return '';
+ }
+
+ $icon = $paths[ $icon_key ];
+ $extra_attrs = isset( $icon['fill-rule'] ) ? ' fill-rule="' . esc_attr( $icon['fill-rule'] ) . '"' : '';
+
+ return sprintf(
+ '
',
+ esc_attr( $icon['d'] ),
+ $extra_attrs
+ );
+}
+
/**
* Make default texts available to the editor.
*/
diff --git a/projects/plugins/jetpack/extensions/blocks/donations/edit.js b/projects/plugins/jetpack/extensions/blocks/donations/edit.js
index 49af60a41f6d..f11307145ef1 100644
--- a/projects/plugins/jetpack/extensions/blocks/donations/edit.js
+++ b/projects/plugins/jetpack/extensions/blocks/donations/edit.js
@@ -1,5 +1,5 @@
import { useBlockProps } from '@wordpress/block-editor';
-import { Spinner } from '@wordpress/components';
+import { Icon, Spinner } from '@wordpress/components';
import { useInstanceId } from '@wordpress/compose';
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback, useState, useEffect } from '@wordpress/element';
@@ -12,9 +12,11 @@ import useIsUserConnected from '../../shared/use-is-user-connected';
import { store as membershipProductsStore } from '../../store/membership-products';
import { STORE_NAME as MEMBERSHIPS_PRODUCTS_STORE } from '../../store/membership-products/constants';
import buildCustomStyles from './build-custom-styles';
+import Controls from './controls';
import fetchDefaultProducts from './fetch-default-products';
import fetchStatus from './fetch-status';
import FirstTimeModal from './first-time-modal';
+import { TRIGGER_ICONS } from './icons';
import './first-time-modal.scss';
import LoadingError from './loading-error';
import StyleControls from './style-controls';
@@ -22,7 +24,15 @@ import Tabs from './tabs';
const Edit = props => {
const { attributes, setAttributes } = props;
- const { currency, tabsAppearance, className } = attributes;
+ const {
+ currency,
+ tabsAppearance,
+ className,
+ displayMode,
+ triggerButtonText,
+ triggerIcon,
+ triggerSticky,
+ } = attributes;
// Migrate legacy blocks that used the block-style variation
// (`is-style-buttons` saved into `className`) over to the new
@@ -45,8 +55,14 @@ const Edit = props => {
const instanceId = useInstanceId( Edit, 'jp-donations' );
const customStyles = buildCustomStyles( attributes, `.${ instanceId }` );
- const wrapperClassName =
- tabsAppearance === 'buttons' ? `${ instanceId } is-style-buttons` : instanceId;
+ const wrapperClassName = [
+ instanceId,
+ tabsAppearance === 'buttons' && 'is-style-buttons',
+ displayMode === 'modal' && 'is-display-modal',
+ displayMode === 'modal' && triggerSticky && 'is-sticky',
+ ]
+ .filter( Boolean )
+ .join( ' ' );
const blockProps = useBlockProps( { className: wrapperClassName } );
const [ loadingError, setLoadingError ] = useState( '' );
const [ products, setProducts ] = useState( [] );
@@ -206,6 +222,24 @@ const Edit = props => {
} else if ( ! currency ) {
// Memberships settings are still loading
content =
;
+ } else if ( displayMode === 'modal' ) {
+ const triggerIconEntry = TRIGGER_ICONS.find( ( { key } ) => key === triggerIcon );
+ const triggerLabel = triggerButtonText || __( 'Donate', 'jetpack' );
+ content = (
+ <>
+
+
+ >
+ );
} else {
content =
;
}
diff --git a/projects/plugins/jetpack/extensions/blocks/donations/editor.scss b/projects/plugins/jetpack/extensions/blocks/donations/editor.scss
index f498191b0b76..bc3620144225 100644
--- a/projects/plugins/jetpack/extensions/blocks/donations/editor.scss
+++ b/projects/plugins/jetpack/extensions/blocks/donations/editor.scss
@@ -44,6 +44,20 @@
.jetpack-block-nudge {
max-width: none;
}
+
+ // Trigger button preview (pop-up display mode).
+ .donations__trigger-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ cursor: default;
+ pointer-events: none;
+ }
+
+ .donations__trigger-icon {
+ flex-shrink: 0;
+ fill: currentColor;
+ }
}
.jetpack-donations__currency-toggle {
@@ -73,6 +87,40 @@
border-bottom: 0;
}
+// Icon picker grid in the Display panel.
+.jetpack-donations__icon-picker {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 4px;
+ margin-block-start: 4px;
+
+ .jetpack-donations__icon-option {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ min-height: 36px;
+ padding: 4px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background: transparent;
+ cursor: pointer;
+
+ &:hover {
+ border-color: #1e1e1e;
+ }
+
+ &.is-pressed,
+ &.is-pressed:hover {
+ border-color: var(--wp-components-color-foreground, #1e1e1e);
+ }
+
+ svg {
+ display: block;
+ }
+ }
+}
+
// Make labels inside our custom style panels inherit text color from the
// inspector sidebar (`.interface-complementary-area`) instead of the
// component default grey, matching the auto-rendered supports panels.
diff --git a/projects/plugins/jetpack/extensions/blocks/donations/icons.js b/projects/plugins/jetpack/extensions/blocks/donations/icons.js
new file mode 100644
index 000000000000..24f248dbe4c0
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/donations/icons.js
@@ -0,0 +1,56 @@
+import { __ } from '@wordpress/i18n';
+import { gift } from '@wordpress/icons';
+import { SVG, Path } from '@wordpress/primitives';
+
+// Heart — reuses the Donations block's own block.json icon path.
+const heart = (
+
+);
+
+// Smiley face.
+const smiley = (
+
+);
+
+// Coffee cup (hot beverage mug with handle and steam).
+const coffee = (
+
+);
+
+// SVG path strings used by PHP (donations.php) for server-side render.
+// Keep in sync with the React icon definitions above and @wordpress/icons source.
+export const ICON_SVG_PATHS = {
+ heart:
+ 'M16.5 4.5c2.206 0 4 1.794 4 4 0 4.67-5.543 8.94-8.5 11.023C9.043 17.44 3.5 13.17 3.5 8.5c0-2.206 1.794-4 4-4 1.298 0 2.522.638 3.273 1.706L12 7.953l1.227-1.746c.75-1.07 1.975-1.707 3.273-1.707m0-1.5c-1.862 0-3.505.928-4.5 2.344C11.005 3.928 9.362 3 7.5 3 4.462 3 2 5.462 2 8.5c0 5.72 6.5 10.438 10 12.85 3.5-2.412 10-7.13 10-12.85C22 5.462 19.538 3 16.5 3z',
+ gift: 'M15.333 4C16.6677 4 17.75 5.0823 17.75 6.41699V6.75C17.75 7.20058 17.6394 7.62468 17.4473 8H18.5C19.2767 8 19.9154 8.59028 19.9922 9.34668L20 9.5V18.5C20 19.3284 19.3284 20 18.5 20H5.5C4.72334 20 4.08461 19.4097 4.00781 18.6533L4 18.5V9.5L4.00781 9.34668C4.07949 8.64069 4.64069 8.07949 5.34668 8.00781L5.5 8H6.55273C6.36065 7.62468 6.25 7.20058 6.25 6.75V6.41699C6.25 5.0823 7.3323 4 8.66699 4C10.0436 4.00011 11.2604 4.68183 12 5.72559C12.7396 4.68183 13.9564 4.00011 15.333 4ZM5.5 18.5H11.25V9.5H5.5V18.5ZM12.75 18.5H18.5V9.5H12.75V18.5ZM8.66699 5.5C8.16073 5.5 7.75 5.91073 7.75 6.41699V6.75C7.75 7.44036 8.30964 8 9 8H11.2461C11.2021 6.61198 10.0657 5.50017 8.66699 5.5ZM15.333 5.5C13.9343 5.50017 12.7979 6.61198 12.7539 8H15C15.6904 8 16.25 7.44036 16.25 6.75V6.41699C16.25 5.91073 15.8393 5.5 15.333 5.5Z',
+ star: 'M11.776 4.454a.25.25 0 01.448 0l2.069 4.192a.25.25 0 00.188.137l4.626.672a.25.25 0 01.139.426l-3.348 3.263a.25.25 0 00-.072.222l.79 4.607a.25.25 0 01-.362.263l-4.138-2.175a.25.25 0 00-.232 0l-4.138 2.175a.25.25 0 01-.363-.263l.79-4.607a.25.25 0 00-.071-.222L4.754 9.881a.25.25 0 01.139-.426l4.626-.672a.25.25 0 00.188-.137l2.069-4.192z',
+ 'thumbs-up':
+ 'm3 12 1 8h1.5l-1-8H3Zm15.8-2h-4.4l.8-3.6c.3-1.3-.7-2.4-1.9-2.4h-.2c-.6 0-1.2.3-1.6.8l-5 6.6c-.3.4-.4.8-.4 1.2v.2l.7 5.4v.2c.2.9 1 1.5 1.9 1.5h8.2c.9 0 1.7-.6 1.9-1.4l1.8-6c.4-1.3-.6-2.6-1.9-2.6Zm.5 2.1-1.8 6c0 .2-.3.4-.5.4H8.8c-.3 0-.5-.2-.5-.4l-.7-5.4v-.4l5-6.6c0-.1.2-.2.4-.2h.2c.3 0 .6.3.5.6l-.8 3.6c-.1.4 0 .9.3 1.3s.7.6 1.2.6h4.4c.3 0 .6.3.5.6Z',
+ smiley:
+ 'M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5 14.67 11 15.5 11zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z',
+ coffee:
+ 'M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.89 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4zM9 1v2M12 1v2M15 1v2',
+ 'tip-jar':
+ 'M9 3h6c0-1.1-.9-2-2-2H11C9.9 1 9 1.9 9 3zm7 0H8c-.55 0-1 .45-1 1v1.5c0 .55.45 1 1 1h8c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1zm-1 3.5H9V19c0 1.1.9 2 2 2h2c1.1 0 2-.9 2-2V6.5zm-3 1.5h2v1.5l-1 1.5-1-1.5V8z',
+ 'hand-heart':
+ 'M15.5 2.1c-1.1 0-2 .6-2.5 1.4-.5-.9-1.4-1.4-2.5-1.4C8.8 2.1 7.5 3.4 7.5 5c0 2.5 4.5 5.9 5.5 6.6 1-.7 5.5-4.1 5.5-6.6 0-1.6-1.3-2.9-3-2.9zM9 14H7l-2 7h14l-2-7h-2l-1 3H10l-1-3z',
+ people:
+ 'M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z',
+};
+
+// Ordered list used to build the icon picker row (4 icons).
+// Keys match ICON_SVG_PATHS and the triggerIcon block attribute.
+export const TRIGGER_ICONS = [
+ { key: 'heart', label: __( 'Heart', 'jetpack' ), icon: heart },
+ { key: 'gift', label: __( 'Gift', 'jetpack' ), icon: gift },
+ { key: 'smiley', label: __( 'Smiley', 'jetpack' ), icon: smiley },
+ { key: 'coffee', label: __( 'Cup', 'jetpack' ), icon: coffee },
+];
+
+export { gift };
diff --git a/projects/plugins/jetpack/extensions/blocks/donations/view.js b/projects/plugins/jetpack/extensions/blocks/donations/view.js
index 7a5d7ca205db..44e9fe2d3112 100644
--- a/projects/plugins/jetpack/extensions/blocks/donations/view.js
+++ b/projects/plugins/jetpack/extensions/blocks/donations/view.js
@@ -323,8 +323,79 @@ class JetpackDonations {
}
}
+class JetpackDonationsModal {
+ constructor( block ) {
+ this.block = block;
+ this.trigger = block.querySelector( '.donations__trigger-button' );
+ this.overlay = block.querySelector( '.donations__modal-overlay' );
+ this.closeBtn = block.querySelector( '.donations__modal-close' );
+
+ if ( ! this.trigger || ! this.overlay ) {
+ return;
+ }
+
+ this.trigger.addEventListener( 'click', () => this.open() );
+ this.closeBtn?.addEventListener( 'click', () => this.close() );
+ this.overlay.addEventListener( 'click', event => {
+ if ( event.target === this.overlay ) {
+ this.close();
+ }
+ } );
+ document.addEventListener( 'keydown', event => {
+ if ( event.key === 'Escape' && ! this.overlay.hidden ) {
+ this.close();
+ }
+ } );
+ }
+
+ open() {
+ this.overlay.hidden = false;
+ this.overlay.ownerDocument.body.classList.add( 'donations-modal-open' );
+ this._previousFocus = this.overlay.ownerDocument.activeElement;
+ const firstFocusable = this.overlay.querySelector(
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+ );
+ firstFocusable?.focus();
+ this.overlay.addEventListener( 'keydown', this._trapFocus );
+ }
+
+ close() {
+ this.overlay.hidden = true;
+ this.overlay.ownerDocument.body.classList.remove( 'donations-modal-open' );
+ this.overlay.removeEventListener( 'keydown', this._trapFocus );
+ this._previousFocus?.focus();
+ }
+
+ _trapFocus = event => {
+ if ( event.key !== 'Tab' ) {
+ return;
+ }
+ const focusable = Array.from(
+ this.overlay.querySelectorAll(
+ 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
+ )
+ ).filter( el => ! el.closest( '[hidden]' ) );
+ if ( ! focusable.length ) {
+ return;
+ }
+ const first = focusable[ 0 ];
+ const last = focusable[ focusable.length - 1 ];
+ if ( event.shiftKey && this.overlay.ownerDocument.activeElement === first ) {
+ event.preventDefault();
+ last.focus();
+ } else if ( ! event.shiftKey && this.overlay.ownerDocument.activeElement === last ) {
+ event.preventDefault();
+ first.focus();
+ }
+ };
+}
+
domReady( () => {
const blocks = document.querySelectorAll( '.wp-block-jetpack-donations' );
- blocks.forEach( block => new JetpackDonations( block ) );
+ blocks.forEach( block =>
+ block.querySelector( '.donations__modal-overlay' )
+ ? [ new JetpackDonationsModal( block ), new JetpackDonations( block ) ]
+ : new JetpackDonations( block )
+ );
initializeMembershipButtons( '.donations__donate-button' );
} );
diff --git a/projects/plugins/jetpack/extensions/blocks/donations/view.scss b/projects/plugins/jetpack/extensions/blocks/donations/view.scss
index e537c1d3b7eb..1a73f42d697b 100644
--- a/projects/plugins/jetpack/extensions/blocks/donations/view.scss
+++ b/projects/plugins/jetpack/extensions/blocks/donations/view.scss
@@ -122,4 +122,95 @@
pointer-events: none;
opacity: 0.2;
}
+
+ // Sticky Pop-up mode: fix the trigger button to the bottom corner of the
+ // viewport so it stays visible as visitors scroll. The modal overlay sits
+ // at z-index 100000 so it always floats above the sticky button.
+ &.is-display-modal.is-sticky {
+ position: fixed;
+ bottom: 24px;
+ inset-inline-end: 24px;
+ z-index: 9999;
+ }
+
+ // Trigger button: icon + text inline.
+ .donations__trigger-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .donations__trigger-icon {
+ flex-shrink: 0;
+ fill: currentColor;
+ }
+
+ // Modal overlay: covers viewport, blurred backdrop.
+ .donations__modal-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 100000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.45);
+ backdrop-filter: blur(5px);
+
+ &[hidden] {
+ display: none;
+ }
+ }
+
+ // Modal dialog container — clips to border-radius; border/radius set via
+ // build_custom_styles when the block has border settings configured.
+ .donations__modal-dialog {
+ position: relative;
+ width: 90%;
+ max-width: 640px;
+ max-height: 90vh;
+ overflow: hidden;
+ background: var(--wp--style--color--background, #fff);
+ box-shadow: 0 8px 40px rgba(0, 0, 0, 0.18);
+
+ @media (max-width: 600px) {
+ width: 92%;
+ }
+ }
+
+ .donations__modal-content {
+ overflow-y: auto;
+ max-height: 90vh;
+ padding: 40px;
+ box-sizing: border-box;
+
+ @media (max-width: 600px) {
+ padding: 24px 20px;
+ }
+ }
+
+ // Close button: X in upper-right corner.
+ .donations__modal-close {
+ position: absolute;
+ inset-block-start: 12px;
+ inset-inline-end: 12px;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 6px;
+ line-height: 1;
+ color: inherit;
+ font-size: 18px;
+ opacity: 0.6;
+ border-radius: 4px;
+
+ &:hover,
+ &:focus-visible {
+ opacity: 1;
+ }
+ }
+}
+
+// Prevent body scroll when modal is open.
+body.donations-modal-open {
+ overflow: hidden;
}