From b19c079078bc07d85d4ad5fff87d91fa0963bcb6 Mon Sep 17 00:00:00 2001 From: Kelly Dwan Date: Wed, 18 Dec 2024 15:25:23 -0500 Subject: [PATCH] New block: Modal block using the Interactivity API (#675) * Add a Modal block * Add editor UI * Add color controls * Modal style: Use default content width * Remove editor CSS The frontend style is loaded in the editor iframe, so this is not used * Dry the color code, explain how values are set * Render button as a div in the editor to prevent keyboard events * Fix or ignore eslint issues * Add button style options * Add `is-small` style options * Fix linter --- mu-plugins/blocks/modal/index.php | 62 +++++++ mu-plugins/blocks/modal/render.php | 81 +++++++++ mu-plugins/blocks/modal/src/block.json | 60 +++++++ mu-plugins/blocks/modal/src/index.js | 227 +++++++++++++++++++++++++ mu-plugins/blocks/modal/src/style.scss | 73 ++++++++ mu-plugins/blocks/modal/src/view.js | 93 ++++++++++ mu-plugins/loader.php | 1 + 7 files changed, 597 insertions(+) create mode 100644 mu-plugins/blocks/modal/index.php create mode 100644 mu-plugins/blocks/modal/render.php create mode 100644 mu-plugins/blocks/modal/src/block.json create mode 100644 mu-plugins/blocks/modal/src/index.js create mode 100644 mu-plugins/blocks/modal/src/style.scss create mode 100644 mu-plugins/blocks/modal/src/view.js diff --git a/mu-plugins/blocks/modal/index.php b/mu-plugins/blocks/modal/index.php new file mode 100644 index 00000000..241be329 --- /dev/null +++ b/mu-plugins/blocks/modal/index.php @@ -0,0 +1,62 @@ + false, +]; + +// Set up a unique ID for this modal. +$html_id = wp_unique_id( 'modal-' ); + +?> +
$style ]); // phpcs:ignore ?> + data-wp-interactive="wporg/modal" + data-wp-watch="callbacks.init" + data-wp-on--keydown="actions.handleKeydown" + data-wp-class--is-modal-open="context.isOpen" + +> +
+
+ + + + + +
+
+ +
+
+ + +
+ +
+
+
+
diff --git a/mu-plugins/blocks/modal/src/block.json b/mu-plugins/blocks/modal/src/block.json new file mode 100644 index 00000000..ce00b178 --- /dev/null +++ b/mu-plugins/blocks/modal/src/block.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "wporg/modal", + "title": "Modal", + "icon": "location", + "category": "layout", + "description": "A modal dialog.", + "textdomain": "wporg", + "attributes": { + "backgroundColor": { + "type": "string", + "default": "white" + }, + "customBackgroundColor": { + "type": "string" + }, + "textColor": { + "type": "string", + "default": "charcoal-1" + }, + "customTextColor": { + "type": "string" + }, + "closeButtonColor": { + "type": "string", + "default": "charcoal-1" + }, + "customCloseButtonColor": { + "type": "string" + }, + "overlayColor": { + "type": "string" + }, + "customOverlayColor": { + "type": "string", + "default": "#1e1e1ecc" + }, + "href": { + "type": "string" + }, + "label": { + "type": "string", + "default": "Open modal" + }, + "buttonStyle": { + "type": "string", + "default": "" + } + }, + "supports": { + "align": false, + "layout": false, + "interactivity": true + }, + "editorScript": "file:./index.js", + "style": "file:./style-index.css", + "viewScriptModule": "file:./view.js", + "render": "file:../render.php" +} diff --git a/mu-plugins/blocks/modal/src/index.js b/mu-plugins/blocks/modal/src/index.js new file mode 100644 index 00000000..f4606217 --- /dev/null +++ b/mu-plugins/blocks/modal/src/index.js @@ -0,0 +1,227 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- experimental ok. + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + InnerBlocks, + InspectorControls, + RichText, + useBlockProps, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- experimental ok. + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, + withColors, +} from '@wordpress/block-editor'; +import { PanelBody, SelectControl, TextControl, ToggleControl } from '@wordpress/components'; +import { store as blocksStore, registerBlockType } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { useState } from 'react'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import './style.scss'; + +function Edit( { + attributes, + setAttributes, + backgroundColor, + setBackgroundColor, + textColor, + setTextColor, + closeButtonColor, + setCloseButtonColor, + overlayColor, + setOverlayColor, + clientId, +} ) { + const [ isModalPreview, setIsModalPreview ] = useState( false ); + const [ buttonStyleOptions, setButtonStyleOptions ] = useState( [] ); + const { customBackgroundColor, customCloseButtonColor, customTextColor, customOverlayColor } = attributes; + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + + useSelect( ( select ) => { + const { getBlockStyles } = select( blocksStore ); + const styles = getBlockStyles( 'core/button' ); + const options = styles.map( ( item ) => ( { label: item.label, value: item.name } ) ); + // Add the same options with the `is-small` modifier. + styles.forEach( ( item ) => { + options.push( { label: `${ item.label } (small)`, value: `${ item.name } is-small` } ); + } ); + setButtonStyleOptions( options ); + }, [] ); + + const classes = []; + if ( isModalPreview ) { + classes.push( 'is-modal-open' ); + } + + const style = { + '--wp--custom--wporg-modal--color--background': backgroundColor.slug + ? `var( --wp--preset--color--${ backgroundColor.slug } )` + : customBackgroundColor, + '--wp--custom--wporg-modal--color--text': textColor.slug + ? `var( --wp--preset--color--${ textColor.slug } )` + : customTextColor, + '--wp--custom--wporg-modal--color--close-button': closeButtonColor.slug + ? `var( --wp--preset--color--${ closeButtonColor.slug } )` + : customCloseButtonColor, + '--wp--custom--wporg-modal--color--overlay': overlayColor.slug + ? `var( --wp--preset--color--${ overlayColor.slug } )` + : customOverlayColor, + }; + + const blockProps = useBlockProps( { + className: classes, + style: style, + } ); + + return ( + <> + + { + setBackgroundColor( value ); + setAttributes( { + customBackgroundColor: value, + } ); + }, + }, + { + label: __( 'Modal text', 'wporg' ), + colorValue: textColor.color || customTextColor, + onColorChange: ( value ) => { + setTextColor( value ); + setAttributes( { + customTextColor: value, + } ); + }, + }, + { + label: __( 'Close button', 'wporg' ), + colorValue: closeButtonColor.color || customCloseButtonColor, + onColorChange: ( value ) => { + setCloseButtonColor( value ); + setAttributes( { + customCloseButtonColor: value, + } ); + }, + }, + { + label: __( 'Overlay', 'wporg' ), + colorValue: overlayColor.color || customOverlayColor, + onColorChange: ( value ) => { + setOverlayColor( value ); + setAttributes( { + customOverlayColor: value, + } ); + }, + enableAlpha: true, + }, + ] } + panelId={ clientId } + hasColorsOrGradients={ false } + disableCustomColors={ false } + __experimentalIsRenderedInSidebar + { ...colorGradientSettings } + /> + + + + { + setIsModalPreview( newValue ); + } } + /> + setAttributes( { href } ) } + /> + { + setAttributes( { buttonStyle: newValue } ); + } } + value={ attributes.buttonStyle } + options={ buttonStyleOptions } + /> + + +
+
+
+ setAttributes( { label } ) } + placeholder={ __( 'Open modal', 'wporg' ) } + /> +
+
+ +
+ + ); +} + +registerBlockType( metadata.name, { + edit: withColors( { + backgroundColor: 'background-color', + textColor: 'text-color', + closeButtonColor: 'close-button-color', + overlayColor: 'overlay-color', + } )( Edit ), + save: () => { + return ; + }, +} ); diff --git a/mu-plugins/blocks/modal/src/style.scss b/mu-plugins/blocks/modal/src/style.scss new file mode 100644 index 00000000..1e2e8fb6 --- /dev/null +++ b/mu-plugins/blocks/modal/src/style.scss @@ -0,0 +1,73 @@ +:where(.wp-block-wporg-modal) { + --wp--custom--wporg-modal--color--background: var(--wp--preset--color--white); + --wp--custom--wporg-modal--color--text: var(--wp--preset--color--charcoal-1); + --wp--custom--wporg-modal--color--overlay: #1e1e1ecc; + --wp--custom--wporg-modal--color--close-button: var(--wp--preset--color--charcoal-1); +} + +.wp-block-wporg-modal { + position: relative; + + &.is-modal-open { + z-index: 100000; /* admin bar + 1. */ + + & .wporg-modal__modal { + display: block; + } + } +} + +.wporg-modal__modal-backdrop { + position: fixed; + inset: 0; + height: 100vh; + width: 100vw; + padding: 20px; + + &:not([hidden]) { + display: flex; + align-items: center; + justify-content: center; + background: var(--wp--custom--wporg-modal--color--overlay); + } +} + +.wporg-modal__modal { + display: none; + position: relative; + width: var(--wp--custom--layout--content-size, 680px); + background: var(--wp--custom--wporg-modal--color--background); + color: var(--wp--custom--wporg-modal--color--text); +} + +.wporg-modal__modal-close { + position: absolute; + top: var(--wp--preset--spacing--20); + right: var(--wp--preset--spacing--20); + z-index: 1; + width: 24px; + height: 24px; + cursor: pointer; + border: none; + background: transparent; + color: var(--wp--custom--wporg-modal--color--close-button); + + &::before { + content: ""; + display: inline-block; + position: absolute; + inset: 0; + /* stylelint-disable-next-line function-url-quotes */ + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' aria-hidden='true' focusable='false'%3E%3Cpath d='m13.06 12 6.47-6.47-1.06-1.06L12 10.94 5.53 4.47 4.47 5.53 10.94 12l-6.47 6.47 1.06 1.06L12 13.06l6.47 6.47 1.06-1.06L13.06 12Z'%3E%3C/path%3E%3C/svg%3E"); + mask-repeat: no-repeat; + mask-position: center; + background-color: currentcolor; + } +} + +.wporg-modal__modal-content { + margin: 0; + max-height: 80dvh; + overflow-y: auto; + border: none; +} diff --git a/mu-plugins/blocks/modal/src/view.js b/mu-plugins/blocks/modal/src/view.js new file mode 100644 index 00000000..1514d608 --- /dev/null +++ b/mu-plugins/blocks/modal/src/view.js @@ -0,0 +1,93 @@ +/** + * WordPress dependencies + */ +import { getContext, getElement, store } from '@wordpress/interactivity'; + +// See https://github.com/WordPress/gutenberg/blob/37f52ae884a40f7cb77ac2484648b4e4ad973b59/packages/block-library/src/navigation/view-interactivity.js +const focusableSelectors = [ + 'a[href]', + 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])', + 'select:not([disabled]):not([aria-hidden])', + 'textarea:not([disabled]):not([aria-hidden])', + 'button:not([disabled]):not([aria-hidden])', + '[contenteditable]', + '[tabindex]:not([tabindex^="-"])', +]; + +const { actions } = store( 'wporg/modal', { + actions: { + toggle: () => { + const context = getContext(); + if ( context.isOpen ) { + actions.close(); + } else { + actions.open(); + } + }, + + /** + * Close the modal only if the backdrop is clicked. + * Ignores clicks inside the modal itself. + * + * @param {Event} event + */ + clickBackdrop: ( event ) => { + if ( event.target.classList.contains( 'wporg-modal__modal-backdrop' ) ) { + actions.close(); + } + }, + + open: () => { + const context = getContext(); + context.isOpen = true; + context.modal.focus(); + }, + + close: () => { + const context = getContext(); + context.isOpen = false; + context.toggleButton.focus(); + }, + + handleKeydown: ( event ) => { + const context = getContext(); + // Only handle key events if the dropdown is open. + if ( ! context.isOpen ) { + return; + } + + // If Escape close the dropdown. + if ( event.key === 'Escape' ) { + actions.close(); + return; + } + + // Trap focus. + if ( event.key === 'Tab' ) { + // If shift + tab it change the direction. + if ( event.shiftKey && window.document.activeElement === context.firstFocusableElement ) { + event.preventDefault(); + context.lastFocusableElement.focus(); + } else if ( ! event.shiftKey && window.document.activeElement === context.lastFocusableElement ) { + event.preventDefault(); + context.firstFocusableElement.focus(); + } + } + }, + }, + + callbacks: { + init: () => { + const context = getContext(); + const { ref } = getElement(); + context.toggleButton = ref.querySelector( '.wporg-modal__toggle' ); + context.modal = ref.querySelector( '.wporg-modal__modal' ); + + if ( context.isOpen ) { + const focusableElements = context.modal.querySelectorAll( focusableSelectors ); + context.firstFocusableElement = focusableElements[ 0 ]; + context.lastFocusableElement = focusableElements[ focusableElements.length - 1 ]; + } + }, + }, +} ); diff --git a/mu-plugins/loader.php b/mu-plugins/loader.php index 3d08e7ba..8aacabbf 100644 --- a/mu-plugins/loader.php +++ b/mu-plugins/loader.php @@ -37,6 +37,7 @@ require_once __DIR__ . '/blocks/local-navigation-bar/index.php'; require_once __DIR__ . '/blocks/latest-news/latest-news.php'; require_once __DIR__ . '/blocks/link-wrapper/index.php'; +require_once __DIR__ . '/blocks/modal/index.php'; require_once __DIR__ . '/blocks/navigation/index.php'; require_once __DIR__ . '/blocks/notice/index.php'; require_once __DIR__ . '/blocks/query-filter/index.php';