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; }