diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index a7570bf590a39..8e38632113868 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -277,6 +277,45 @@ Display footnotes added to the page. ([Source](https://github.com/WordPress/gute
- **Supports:** color (background, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~, ~~multiple~~, ~~reusable~~
- **Attributes:**
+## Form
+
+A form. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/form))
+
+- **Name:** core/form
+- **Category:** common
+- **Supports:** anchor, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~
+- **Attributes:** action, email, method, submissionMethod
+
+## Input field
+
+The basic building block for forms. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/form-input))
+
+- **Name:** core/form-input
+- **Category:** common
+- **Parent:** core/form
+- **Supports:** anchor, spacing (margin), ~~reusable~~
+- **Attributes:** inlineLabel, label, name, placeholder, required, type, value, visibilityPermissions
+
+## Form Submission Notification
+
+Provide a notification message after the form has been submitted. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/form-submission-notification))
+
+- **Name:** core/form-submission-notification
+- **Category:** common
+- **Parent:** core/form
+- **Supports:**
+- **Attributes:** type
+
+## Form submit button
+
+A submission button for forms. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/form-submit-button))
+
+- **Name:** core/form-submit-button
+- **Category:** common
+- **Parent:** core/form
+- **Supports:**
+- **Attributes:**
+
## Classic
Use the classic WordPress editor. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/freeform))
diff --git a/lib/blocks.php b/lib/blocks.php
index 537fa9ce4b45e..1794762b010db 100644
--- a/lib/blocks.php
+++ b/lib/blocks.php
@@ -22,6 +22,8 @@ function gutenberg_reregister_core_block_types() {
'column',
'columns',
'details',
+ 'form-input',
+ 'form-submit-button',
'group',
'html',
'list',
@@ -66,6 +68,9 @@ function gutenberg_reregister_core_block_types() {
'comments.php' => 'core/comments',
'footnotes.php' => 'core/footnotes',
'file.php' => 'core/file',
+ 'form.php' => 'core/form',
+ 'form-input.php' => 'core/form-input',
+ 'form-submission-notification.php' => 'core/form-submission-notification',
'home-link.php' => 'core/home-link',
'image.php' => 'core/image',
'gallery.php' => 'core/gallery',
diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php
index c09b5cde0f16b..00c929a3312a4 100644
--- a/lib/experimental/editor-settings.php
+++ b/lib/experimental/editor-settings.php
@@ -33,3 +33,15 @@ function gutenberg_enable_experiments() {
}
add_action( 'admin_init', 'gutenberg_enable_experiments' );
+
+/**
+ * Sets a global JS variable used to trigger the availability of form & input blocks.
+ */
+function gutenberg_enable_form_input_blocks() {
+ $gutenberg_experiments = get_option( 'gutenberg-experiments' );
+ if ( $gutenberg_experiments && array_key_exists( 'gutenberg-form-blocks', $gutenberg_experiments ) ) {
+ wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableFormBlocks = true', 'before' );
+ }
+}
+
+add_action( 'admin_init', 'gutenberg_enable_form_input_blocks' );
diff --git a/lib/experimental/kses-allowed-html.php b/lib/experimental/kses-allowed-html.php
new file mode 100644
index 0000000000000..122faef7b4ca2
--- /dev/null
+++ b/lib/experimental/kses-allowed-html.php
@@ -0,0 +1,43 @@
+ array(),
+ 'name' => array(),
+ 'value' => array(),
+ 'checked' => array(),
+ 'required' => array(),
+ 'aria-required' => array(),
+ 'class' => array(),
+ );
+
+ $allowedtags['label'] = array(
+ 'for' => array(),
+ 'class' => array(),
+ );
+
+ $allowedtags['textarea'] = array(
+ 'name' => array(),
+ 'required' => array(),
+ 'aria-required' => array(),
+ 'class' => array(),
+ );
+ return $allowedtags;
+}
+add_filter( 'wp_kses_allowed_html', 'gutenberg_kses_allowed_html', 10, 2 );
diff --git a/lib/experiments-page.php b/lib/experiments-page.php
index 133d968ba2cb7..90a88fd959288 100644
--- a/lib/experiments-page.php
+++ b/lib/experiments-page.php
@@ -78,6 +78,17 @@ function gutenberg_initialize_experiments_settings() {
'id' => 'gutenberg-color-randomizer',
)
);
+ add_settings_field(
+ 'gutenberg-form-blocks',
+ __( 'Form and input blocks ', 'gutenberg' ),
+ 'gutenberg_display_experiment_field',
+ 'gutenberg-experiments',
+ 'gutenberg_experiments_section',
+ array(
+ 'label' => __( 'Test new blocks to allow building forms (Warning: The new feature is not ready. You may experience UX issues that are being addressed)', 'gutenberg' ),
+ 'id' => 'gutenberg-form-blocks',
+ )
+ );
add_settings_field(
'gutenberg-group-grid-variation',
diff --git a/lib/load.php b/lib/load.php
index d87c53081903e..a3a61407764b5 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -66,6 +66,8 @@ function gutenberg_is_experiment_enabled( $name ) {
}
require_once __DIR__ . '/experimental/class-gutenberg-rest-template-revision-count.php';
require_once __DIR__ . '/experimental/rest-api.php';
+
+ require_once __DIR__ . '/experimental/kses-allowed-html.php';
}
require __DIR__ . '/experimental/editor-settings.php';
diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss
index 07c58599c5098..5f3d962ae7afa 100644
--- a/packages/block-library/src/editor.scss
+++ b/packages/block-library/src/editor.scss
@@ -16,6 +16,8 @@
@import "./details/editor.scss";
@import "./embed/editor.scss";
@import "./file/editor.scss";
+@import "./form-input/editor.scss";
+@import "./form-submission-notification/editor.scss";
@import "./freeform/editor.scss";
@import "./gallery/editor.scss";
@import "./group/editor.scss";
diff --git a/packages/block-library/src/form-input/block.json b/packages/block-library/src/form-input/block.json
new file mode 100644
index 0000000000000..dbe182f03b499
--- /dev/null
+++ b/packages/block-library/src/form-input/block.json
@@ -0,0 +1,72 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "core/form-input",
+ "title": "Input field",
+ "category": "common",
+ "parent": [ "core/form" ],
+ "description": "The basic building block for forms.",
+ "keywords": [ "input", "form" ],
+ "textdomain": "default",
+ "icon": "forms",
+ "attributes": {
+ "type": {
+ "type": "string",
+ "default": "text"
+ },
+ "name": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string",
+ "default": "Label",
+ "selector": ".wp-block-form-input__label-content",
+ "source": "html",
+ "__experimentalRole": "content"
+ },
+ "inlineLabel": {
+ "type": "boolean",
+ "default": false
+ },
+ "required": {
+ "type": "boolean",
+ "default": false,
+ "selector": ".wp-block-form-input__input",
+ "source": "attribute",
+ "attribute": "required"
+ },
+ "placeholder": {
+ "type": "string",
+ "selector": ".wp-block-form-input__input",
+ "source": "attribute",
+ "attribute": "placeholder",
+ "__experimentalRole": "content"
+ },
+ "value": {
+ "type": "string",
+ "default": "",
+ "selector": "input",
+ "source": "attribute",
+ "attribute": "value"
+ },
+ "visibilityPermissions": {
+ "type": "string",
+ "default": "all"
+ }
+ },
+ "supports": {
+ "anchor": true,
+ "reusable": false,
+ "spacing": {
+ "margin": [ "top", "bottom" ]
+ },
+ "__experimentalBorder": {
+ "radius": true,
+ "__experimentalSkipSerialization": true,
+ "__experimentalDefaultControls": {
+ "radius": true
+ }
+ }
+ },
+ "style": [ "wp-block-form-input" ]
+}
diff --git a/packages/block-library/src/form-input/edit.js b/packages/block-library/src/form-input/edit.js
new file mode 100644
index 0000000000000..0742c22c22f42
--- /dev/null
+++ b/packages/block-library/src/form-input/edit.js
@@ -0,0 +1,151 @@
+/**
+ * External dependencies
+ */
+import classNames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ InspectorControls,
+ RichText,
+ useBlockProps,
+ __experimentalUseBorderProps as useBorderProps,
+ __experimentalUseColorProps as useColorProps,
+} from '@wordpress/block-editor';
+import { PanelBody, TextControl, CheckboxControl } from '@wordpress/components';
+
+import { useRef } from '@wordpress/element';
+
+function InputFieldBlock( { attributes, setAttributes, className } ) {
+ const { type, name, label, inlineLabel, required, placeholder, value } =
+ attributes;
+ const blockProps = useBlockProps();
+ const ref = useRef();
+ const TagName = type === 'textarea' ? 'textarea' : 'input';
+
+ const borderProps = useBorderProps( attributes );
+ const colorProps = useColorProps( attributes );
+ if ( ref.current ) {
+ ref.current.focus();
+ }
+
+ const controls = (
+ <>
+ { 'hidden' !== type && (
+
+
+ { 'checkbox' !== type && (
+ {
+ setAttributes( {
+ inlineLabel: newVal,
+ } );
+ } }
+ />
+ ) }
+ {
+ setAttributes( {
+ required: newVal,
+ } );
+ } }
+ />
+
+
+ ) }
+
+ {
+ setAttributes( {
+ name: newVal,
+ } );
+ } }
+ help={ __(
+ 'Affects the "name" atribute of the input element, and is used as a name for the form submission results.'
+ ) }
+ />
+
+ >
+ );
+
+ if ( 'hidden' === type ) {
+ return (
+ <>
+ { controls }
+
+ setAttributes( { value: event.target.value } )
+ }
+ />
+ >
+ );
+ }
+
+ return (
+
+ { controls }
+
+
+ setAttributes( { label: newLabel } )
+ }
+ aria-label={ label ? __( 'Label' ) : __( 'Empty label' ) }
+ data-empty={ label ? false : true }
+ placeholder={ __( 'Type the label for this input' ) }
+ />
+
+ setAttributes( { placeholder: event.target.value } )
+ }
+ aria-required={ required }
+ style={ {
+ ...borderProps.style,
+ ...colorProps.style,
+ } }
+ />
+
+
+ );
+}
+
+export default InputFieldBlock;
diff --git a/packages/block-library/src/form-input/editor.scss b/packages/block-library/src/form-input/editor.scss
new file mode 100644
index 0000000000000..2ac67e6615ed4
--- /dev/null
+++ b/packages/block-library/src/form-input/editor.scss
@@ -0,0 +1,24 @@
+.wp-block-form-input {
+ .is-input-hidden {
+ font-size: 0.85em;
+ opacity: 0.3;
+ border: 1px dashed;
+ padding: 0.5em;
+ box-sizing: border-box;
+ background: repeating-linear-gradient(45deg, transparent, transparent 5px, currentColor 5px, currentColor 6px);
+
+ input[type="text"] {
+ background: transparent;
+ }
+ }
+ &.is-selected {
+ .is-input-hidden {
+ opacity: 1;
+ background: none;
+
+ input[type="text"] {
+ background: unset;
+ }
+ }
+ }
+}
diff --git a/packages/block-library/src/form-input/index.js b/packages/block-library/src/form-input/index.js
new file mode 100644
index 0000000000000..b700e0ade6ca7
--- /dev/null
+++ b/packages/block-library/src/form-input/index.js
@@ -0,0 +1,20 @@
+/**
+ * Internal dependencies
+ */
+import initBlock from '../utils/init-block';
+import edit from './edit';
+import metadata from './block.json';
+import save from './save';
+import variations from './variations';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+export const settings = {
+ edit,
+ save,
+ variations,
+};
+
+export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/block-library/src/form-input/index.php b/packages/block-library/src/form-input/index.php
new file mode 100644
index 0000000000000..f905c2bc6e19f
--- /dev/null
+++ b/packages/block-library/src/form-input/index.php
@@ -0,0 +1,45 @@
+ 'render_block_core_form_input',
+ )
+ );
+}
+add_action( 'init', 'register_block_core_form_input' );
diff --git a/packages/block-library/src/form-input/init.js b/packages/block-library/src/form-input/init.js
new file mode 100644
index 0000000000000..79f0492c2cb2f
--- /dev/null
+++ b/packages/block-library/src/form-input/init.js
@@ -0,0 +1,6 @@
+/**
+ * Internal dependencies
+ */
+import { init } from './';
+
+export default init();
diff --git a/packages/block-library/src/form-input/save.js b/packages/block-library/src/form-input/save.js
new file mode 100644
index 0000000000000..0cca31ca423ee
--- /dev/null
+++ b/packages/block-library/src/form-input/save.js
@@ -0,0 +1,83 @@
+/**
+ * External dependencies
+ */
+import classNames from 'classnames';
+import removeAccents from 'remove-accents';
+
+/**
+ * WordPress dependencies
+ */
+import {
+ RichText,
+ __experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles,
+ __experimentalGetColorClassesAndStyles as getColorClassesAndStyles,
+} from '@wordpress/block-editor';
+
+/**
+ * Get the name attribute from a content string.
+ *
+ * @param {string} content The block content.
+ *
+ * @return {string} Returns the slug.
+ */
+const getNameFromLabel = ( content ) => {
+ const dummyElement = document.createElement( 'div' );
+ dummyElement.innerHTML = content;
+ // Get the slug.
+ return (
+ removeAccents( dummyElement.innerText )
+ // Convert anything that's not a letter or number to a hyphen.
+ .replace( /[^\p{L}\p{N}]+/gu, '-' )
+ // Convert to lowercase
+ .toLowerCase()
+ // Remove any remaining leading or trailing hyphens.
+ .replace( /(^-+)|(-+$)/g, '' )
+ );
+};
+
+export default function save( { attributes } ) {
+ const { type, name, label, inlineLabel, required, placeholder, value } =
+ attributes;
+
+ const borderProps = getBorderClassesAndStyles( attributes );
+ const colorProps = getColorClassesAndStyles( attributes );
+
+ const inputStyle = {
+ ...borderProps.style,
+ ...colorProps.style,
+ };
+
+ const inputClasses = classNames(
+ 'wp-block-form-input__input',
+ colorProps.className,
+ borderProps.className
+ );
+ const TagName = type === 'textarea' ? 'textarea' : 'input';
+
+ if ( 'hidden' === type ) {
+ return ;
+ }
+
+ /* eslint-disable jsx-a11y/label-has-associated-control */
+ return (
+
+ );
+ /* eslint-enable jsx-a11y/label-has-associated-control */
+}
diff --git a/packages/block-library/src/form-input/style.scss b/packages/block-library/src/form-input/style.scss
new file mode 100644
index 0000000000000..d45fc8d7f1f72
--- /dev/null
+++ b/packages/block-library/src/form-input/style.scss
@@ -0,0 +1,61 @@
+.wp-block-form-input__label {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25em;
+ margin-bottom: 0.5em;
+
+ &.is-label-inline {
+ flex-direction: row;
+ gap: 0.5em;
+ align-items: center;
+
+ .wp-block-form-input__label-content {
+ margin-bottom: 0.5em;
+ }
+ }
+
+ /*
+ Small tweak to left-align the checkbox.
+ Even though `:has` is not currently supported in Firefox, this is a small tweak
+ and does not affect the functionality of the block or the user's experience.
+ There will be a minor inconsistency between browsers. However, it's more important to provide
+ a better experience for 80+% of users, until Firefox catches up and supports `:has`.
+ */
+ &:has(input[type="checkbox"]) {
+ width: fit-content;
+ flex-direction: row-reverse;
+ }
+}
+
+.wp-block-form-input__label-content {
+ width: fit-content;
+}
+
+.wp-block-form-input__input {
+ padding: 0 0.5em;
+ font-size: 1em;
+ margin-bottom: 0.5em;
+
+ &[type="text"],
+ &[type="password"],
+ &[type="date"],
+ &[type="datetime"],
+ &[type="datetime-local"],
+ &[type="email"],
+ &[type="month"],
+ &[type="number"],
+ &[type="search"],
+ &[type="tel"],
+ &[type="time"],
+ &[type="url"],
+ &[type="week"] {
+ min-height: 2em;
+ line-height: 2;
+ border: 1px solid;
+ }
+}
+
+textarea.wp-block-form-input__input {
+ min-height: 10em;
+}
diff --git a/packages/block-library/src/form-input/variations.js b/packages/block-library/src/form-input/variations.js
new file mode 100644
index 0000000000000..cc205feb89501
--- /dev/null
+++ b/packages/block-library/src/form-input/variations.js
@@ -0,0 +1,82 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+const variations = [
+ {
+ name: 'text',
+ title: __( 'Text input' ),
+ icon: 'edit-page',
+ description: __( 'A generic text input.' ),
+ attributes: { type: 'text' },
+ isDefault: true,
+ scope: [ 'inserter', 'transform' ],
+ isActive: ( blockAttributes ) =>
+ ! blockAttributes?.type || blockAttributes?.type === 'text',
+ },
+ {
+ name: 'textarea',
+ title: __( 'Textarea input' ),
+ icon: 'testimonial',
+ description: __(
+ 'A textarea input to allow entering multiple lines of text.'
+ ),
+ attributes: { type: 'textarea' },
+ isDefault: true,
+ scope: [ 'inserter', 'transform' ],
+ isActive: ( blockAttributes ) => blockAttributes?.type === 'textarea',
+ },
+ {
+ name: 'checkbox',
+ title: __( 'Checkbox input' ),
+ description: __( 'A simple checkbox input.' ),
+ icon: 'forms',
+ attributes: { type: 'checkbox', inlineLabel: true },
+ isDefault: true,
+ scope: [ 'inserter', 'transform' ],
+ isActive: ( blockAttributes ) => blockAttributes?.type === 'checkbox',
+ },
+ {
+ name: 'email',
+ title: __( 'Email input' ),
+ icon: 'email',
+ description: __( 'Used for email addresses.' ),
+ attributes: { type: 'email' },
+ isDefault: true,
+ scope: [ 'inserter', 'transform' ],
+ isActive: ( blockAttributes ) => blockAttributes?.type === 'email',
+ },
+ {
+ name: 'url',
+ title: __( 'URL input' ),
+ icon: 'admin-site',
+ description: __( 'Used for URLs.' ),
+ attributes: { type: 'url' },
+ isDefault: true,
+ scope: [ 'inserter', 'transform' ],
+ isActive: ( blockAttributes ) => blockAttributes?.type === 'url',
+ },
+ {
+ name: 'tel',
+ title: __( 'Telephone input' ),
+ icon: 'phone',
+ description: __( 'Used for phone numbers.' ),
+ attributes: { type: 'tel' },
+ isDefault: true,
+ scope: [ 'inserter', 'transform' ],
+ isActive: ( blockAttributes ) => blockAttributes?.type === 'tel',
+ },
+ {
+ name: 'number',
+ title: __( 'Number input' ),
+ icon: 'edit-page',
+ description: __( 'A numeric input.' ),
+ attributes: { type: 'number' },
+ isDefault: true,
+ scope: [ 'inserter', 'transform' ],
+ isActive: ( blockAttributes ) => blockAttributes?.type === 'number',
+ },
+];
+
+export default variations;
diff --git a/packages/block-library/src/form-submission-notification/block.json b/packages/block-library/src/form-submission-notification/block.json
new file mode 100644
index 0000000000000..62284d35ab4dd
--- /dev/null
+++ b/packages/block-library/src/form-submission-notification/block.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "core/form-submission-notification",
+ "title": "Form Submission Notification",
+ "category": "common",
+ "parent": [ "core/form" ],
+ "description": "Provide a notification message after the form has been submitted.",
+ "keywords": [ "form", "feedback", "notification", "message" ],
+ "textdomain": "default",
+ "icon": "feedback",
+ "attributes": {
+ "type": {
+ "type": "string",
+ "default": "success"
+ }
+ }
+}
diff --git a/packages/block-library/src/form-submission-notification/edit.js b/packages/block-library/src/form-submission-notification/edit.js
new file mode 100644
index 0000000000000..4425a4d9147df
--- /dev/null
+++ b/packages/block-library/src/form-submission-notification/edit.js
@@ -0,0 +1,63 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ InnerBlocks,
+ useBlockProps,
+ useInnerBlocksProps,
+ store as blockEditorStore,
+} from '@wordpress/block-editor';
+import { useSelect } from '@wordpress/data';
+
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+const TEMPLATE = [
+ [
+ 'core/paragraph',
+ {
+ content: __(
+ "Enter the message you wish displayed for form submission error/success, and select the type of the message (success/error) from the block's options."
+ ),
+ },
+ ],
+];
+
+const Edit = ( { attributes, clientId } ) => {
+ const { type } = attributes;
+ const blockProps = useBlockProps( {
+ className: classnames( 'wp-block-form-submission-notification', {
+ [ `form-notification-type-${ type }` ]: type,
+ } ),
+ } );
+
+ const { hasInnerBlocks } = useSelect(
+ ( select ) => {
+ const { getBlock } = select( blockEditorStore );
+ const block = getBlock( clientId );
+ return {
+ hasInnerBlocks: !! ( block && block.innerBlocks.length ),
+ };
+ },
+ [ clientId ]
+ );
+
+ const innerBlocksProps = useInnerBlocksProps( blockProps, {
+ template: TEMPLATE,
+ renderAppender: hasInnerBlocks
+ ? undefined
+ : InnerBlocks.ButtonBlockAppender,
+ } );
+
+ return (
+
+ );
+};
+export default Edit;
diff --git a/packages/block-library/src/form-submission-notification/editor.scss b/packages/block-library/src/form-submission-notification/editor.scss
new file mode 100644
index 0000000000000..a8d3f4e3d9263
--- /dev/null
+++ b/packages/block-library/src/form-submission-notification/editor.scss
@@ -0,0 +1,45 @@
+.wp-block-form-submission-notification {
+ > * {
+ opacity: 0.25;
+ border: 1px dashed;
+ box-sizing: border-box;
+ background: repeating-linear-gradient(45deg, transparent, transparent 5px, currentColor 5px, currentColor 6px);
+ }
+
+ &.is-selected,
+ &:has(.is-selected) {
+ > * {
+ opacity: 1;
+ background: none;
+ }
+
+ &::after {
+ display: none !important;
+ }
+ }
+
+ &::after {
+ display: flex;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ justify-content: center;
+ align-items: center;
+ font-size: 1.1em;
+ // font-weight: bold;
+ }
+
+ &.form-notification-type-success {
+ &::after {
+ content: attr(data-message-success);
+ }
+ }
+
+ &.form-notification-type-error {
+ &::after {
+ content: attr(data-message-error);
+ }
+ }
+}
diff --git a/packages/block-library/src/form-submission-notification/index.js b/packages/block-library/src/form-submission-notification/index.js
new file mode 100644
index 0000000000000..67c359374eec1
--- /dev/null
+++ b/packages/block-library/src/form-submission-notification/index.js
@@ -0,0 +1,26 @@
+/**
+ * WordPress dependencies
+ */
+import { group as icon } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import initBlock from '../utils/init-block';
+import edit from './edit';
+import metadata from './block.json';
+import save from './save';
+import variations from './variations';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+export const settings = {
+ icon,
+ edit,
+ save,
+ variations,
+};
+
+export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/block-library/src/form-submission-notification/index.php b/packages/block-library/src/form-submission-notification/index.php
new file mode 100644
index 0000000000000..0a57866f37edf
--- /dev/null
+++ b/packages/block-library/src/form-submission-notification/index.php
@@ -0,0 +1,48 @@
+ 'render_block_core_form_submission_notification',
+ )
+ );
+}
+add_action( 'init', 'register_block_core_form_submission_notification' );
diff --git a/packages/block-library/src/form-submission-notification/init.js b/packages/block-library/src/form-submission-notification/init.js
new file mode 100644
index 0000000000000..a7f22ef02d640
--- /dev/null
+++ b/packages/block-library/src/form-submission-notification/init.js
@@ -0,0 +1,6 @@
+/**
+ * Internal dependencies
+ */
+import { init } from '.';
+
+export default init();
diff --git a/packages/block-library/src/form-submission-notification/save.js b/packages/block-library/src/form-submission-notification/save.js
new file mode 100644
index 0000000000000..7b3c6c895c192
--- /dev/null
+++ b/packages/block-library/src/form-submission-notification/save.js
@@ -0,0 +1,28 @@
+/**
+ * WordPress dependencies
+ */
+import { useInnerBlocksProps, useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+export default function save( { attributes } ) {
+ const { type } = attributes;
+
+ return (
+
+ );
+}
diff --git a/packages/block-library/src/form-submission-notification/variations.js b/packages/block-library/src/form-submission-notification/variations.js
new file mode 100644
index 0000000000000..b154a26e5e6a4
--- /dev/null
+++ b/packages/block-library/src/form-submission-notification/variations.js
@@ -0,0 +1,59 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+const variations = [
+ {
+ name: 'form-submission-success',
+ title: __( 'Form submission success' ),
+ description: __( 'Success message for form submissions' ),
+ attributes: {
+ type: 'success',
+ },
+ isDefault: true,
+ innerBlocks: [
+ [
+ 'core/paragraph',
+ {
+ content: __( 'Your form has been submitted successfully.' ),
+ backgroundColor: '#00D084',
+ textColor: '#000000',
+ style: {
+ elements: { link: { color: { text: '#000000' } } },
+ },
+ },
+ ],
+ ],
+ scope: [ 'inserter', 'transform' ],
+ isActive: ( blockAttributes ) =>
+ ! blockAttributes?.type || blockAttributes?.type === 'success',
+ },
+ {
+ name: 'form-submission-error',
+ title: __( 'Form submission error' ),
+ description: __( 'Error/failure message for form submissions' ),
+ attributes: {
+ type: 'error',
+ },
+ isDefault: false,
+ innerBlocks: [
+ [
+ 'core/paragraph',
+ {
+ content: __( 'There was an error submitting your form.' ),
+ backgroundColor: '#CF2E2E',
+ textColor: '#FFFFFF',
+ style: {
+ elements: { link: { color: { text: '#FFFFFF' } } },
+ },
+ },
+ ],
+ ],
+ scope: [ 'inserter', 'transform' ],
+ isActive: ( blockAttributes ) =>
+ ! blockAttributes?.type || blockAttributes?.type === 'error',
+ },
+];
+
+export default variations;
diff --git a/packages/block-library/src/form-submit-button/block.json b/packages/block-library/src/form-submit-button/block.json
new file mode 100644
index 0000000000000..faa938e9bbc24
--- /dev/null
+++ b/packages/block-library/src/form-submit-button/block.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "core/form-submit-button",
+ "title": "Form submit button",
+ "category": "common",
+ "icon": "button",
+ "parent": [ "core/form" ],
+ "description": "A submission button for forms.",
+ "keywords": [ "submit", "button", "form" ],
+ "textdomain": "default",
+ "style": [ "wp-block-form-submit-button" ]
+}
diff --git a/packages/block-library/src/form-submit-button/edit.js b/packages/block-library/src/form-submit-button/edit.js
new file mode 100644
index 0000000000000..f8d7a65c6877a
--- /dev/null
+++ b/packages/block-library/src/form-submit-button/edit.js
@@ -0,0 +1,33 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
+
+const TEMPLATE = [
+ [
+ 'core/buttons',
+ {},
+ [
+ [
+ 'core/button',
+ {
+ text: __( 'Submit' ),
+ tagName: 'button',
+ },
+ ],
+ ],
+ ],
+];
+const Edit = () => {
+ const blockProps = useBlockProps();
+ const innerBlocksProps = useInnerBlocksProps( blockProps, {
+ allowedBlocks: TEMPLATE,
+ template: TEMPLATE,
+ templateLock: 'all',
+ } );
+ return (
+
+ );
+};
+export default Edit;
diff --git a/packages/block-library/src/form-submit-button/index.js b/packages/block-library/src/form-submit-button/index.js
new file mode 100644
index 0000000000000..4c60b5f5c2063
--- /dev/null
+++ b/packages/block-library/src/form-submit-button/index.js
@@ -0,0 +1,18 @@
+/**
+ * Internal dependencies
+ */
+import initBlock from '../utils/init-block';
+import edit from './edit';
+import metadata from './block.json';
+import save from './save';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+export const settings = {
+ edit,
+ save,
+};
+
+export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/block-library/src/form-submit-button/init.js b/packages/block-library/src/form-submit-button/init.js
new file mode 100644
index 0000000000000..79f0492c2cb2f
--- /dev/null
+++ b/packages/block-library/src/form-submit-button/init.js
@@ -0,0 +1,6 @@
+/**
+ * Internal dependencies
+ */
+import { init } from './';
+
+export default init();
diff --git a/packages/block-library/src/form-submit-button/save.js b/packages/block-library/src/form-submit-button/save.js
new file mode 100644
index 0000000000000..ba361ebe9db20
--- /dev/null
+++ b/packages/block-library/src/form-submit-button/save.js
@@ -0,0 +1,14 @@
+/**
+ * WordPress dependencies
+ */
+import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
+
+const Save = () => {
+ const blockProps = useBlockProps.save();
+ return (
+
+
+
+ );
+};
+export default Save;
diff --git a/packages/block-library/src/form-submit-button/style.scss b/packages/block-library/src/form-submit-button/style.scss
new file mode 100644
index 0000000000000..400016b1618d4
--- /dev/null
+++ b/packages/block-library/src/form-submit-button/style.scss
@@ -0,0 +1,3 @@
+.wp-block-form-submit-wrapper {
+ margin-bottom: 0.5em;
+}
diff --git a/packages/block-library/src/form/block.json b/packages/block-library/src/form/block.json
new file mode 100644
index 0000000000000..951d1dce4224e
--- /dev/null
+++ b/packages/block-library/src/form/block.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "core/form",
+ "title": "Form",
+ "category": "common",
+ "description": "A form.",
+ "keywords": [ "container", "wrapper", "row", "section" ],
+ "textdomain": "default",
+ "icon": "feedback",
+ "attributes": {
+ "submissionMethod": {
+ "type": "string",
+ "default": "email"
+ },
+ "method": {
+ "type": "string",
+ "default": "post"
+ },
+ "action": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ }
+ },
+ "supports": {
+ "anchor": true,
+ "className": false,
+ "color": {
+ "gradients": true,
+ "link": true,
+ "__experimentalDefaultControls": {
+ "background": true,
+ "text": true,
+ "link": true
+ }
+ },
+ "spacing": {
+ "margin": true,
+ "padding": true
+ },
+ "typography": {
+ "fontSize": true,
+ "lineHeight": true,
+ "__experimentalFontFamily": true,
+ "__experimentalTextDecoration": true,
+ "__experimentalFontStyle": true,
+ "__experimentalFontWeight": true,
+ "__experimentalLetterSpacing": true,
+ "__experimentalTextTransform": true,
+ "__experimentalDefaultControls": {
+ "fontSize": true
+ }
+ },
+ "__experimentalSelector": "form"
+ },
+ "viewScript": "file:./view.min.js"
+}
diff --git a/packages/block-library/src/form/edit.js b/packages/block-library/src/form/edit.js
new file mode 100644
index 0000000000000..d8ae9ea5e7553
--- /dev/null
+++ b/packages/block-library/src/form/edit.js
@@ -0,0 +1,179 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ InnerBlocks,
+ useBlockProps,
+ useInnerBlocksProps,
+ InspectorControls,
+ store as blockEditorStore,
+} from '@wordpress/block-editor';
+import { TextControl, SelectControl, PanelBody } from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import {
+ formSubmissionNotificationSuccess,
+ formSubmissionNotificationError,
+} from './utils.js';
+
+const ALLOWED_BLOCKS = [
+ 'core/paragraph',
+ 'core/heading',
+ 'core/form-input',
+ 'core/form-submit-button',
+ 'core/form-submission-notification',
+];
+
+const TEMPLATE = [
+ formSubmissionNotificationSuccess,
+ formSubmissionNotificationError,
+ [
+ 'core/form-input',
+ {
+ type: 'text',
+ label: __( 'Name' ),
+ required: true,
+ },
+ ],
+ [
+ 'core/form-input',
+ {
+ type: 'email',
+ label: __( 'Email' ),
+ required: true,
+ },
+ ],
+ [
+ 'core/form-input',
+ {
+ type: 'textarea',
+ label: __( 'Comment' ),
+ required: true,
+ },
+ ],
+ [ 'core/form-submit-button', {} ],
+];
+
+const Edit = ( { attributes, setAttributes, clientId } ) => {
+ const { action, method, email, submissionMethod } = attributes;
+ const blockProps = useBlockProps();
+
+ const { hasInnerBlocks } = useSelect(
+ ( select ) => {
+ const { getBlock } = select( blockEditorStore );
+ const block = getBlock( clientId );
+ return {
+ hasInnerBlocks: !! ( block && block.innerBlocks.length ),
+ };
+ },
+ [ clientId ]
+ );
+
+ const innerBlocksProps = useInnerBlocksProps( blockProps, {
+ allowedBlocks: ALLOWED_BLOCKS,
+ template: TEMPLATE,
+ renderAppender: hasInnerBlocks
+ ? undefined
+ : InnerBlocks.ButtonBlockAppender,
+ } );
+
+ return (
+ <>
+
+
+
+ setAttributes( { submissionMethod: value } )
+ }
+ help={
+ submissionMethod === 'custom'
+ ? __(
+ 'Select the method to use for form submissions. Additional options for the "custom" mode can be found in the "Andvanced" section.'
+ )
+ : __(
+ 'Select the method to use for form submissions.'
+ )
+ }
+ />
+ { submissionMethod === 'email' && (
+ {
+ setAttributes( { email: value } );
+ setAttributes( {
+ action: `mailto:${ value }`,
+ } );
+ setAttributes( { method: 'post' } );
+ } }
+ help={ __(
+ 'The email address where form submissions will be sent. Separate multiple email addresses with a comma.'
+ ) }
+ />
+ ) }
+
+
+ { submissionMethod !== 'email' && (
+
+
+ setAttributes( { method: value } )
+ }
+ help={ __(
+ 'Select the method to use for form submissions.'
+ ) }
+ />
+ {
+ setAttributes( {
+ action: newVal,
+ } );
+ } }
+ help={ __(
+ 'The URL where the form should be submitted.'
+ ) }
+ />
+
+ ) }
+
+ >
+ );
+};
+export default Edit;
diff --git a/packages/block-library/src/form/index.js b/packages/block-library/src/form/index.js
new file mode 100644
index 0000000000000..b700e0ade6ca7
--- /dev/null
+++ b/packages/block-library/src/form/index.js
@@ -0,0 +1,20 @@
+/**
+ * Internal dependencies
+ */
+import initBlock from '../utils/init-block';
+import edit from './edit';
+import metadata from './block.json';
+import save from './save';
+import variations from './variations';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+export const settings = {
+ edit,
+ save,
+ variations,
+};
+
+export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/block-library/src/form/index.php b/packages/block-library/src/form/index.php
new file mode 100644
index 0000000000000..0dbbaf6838740
--- /dev/null
+++ b/packages/block-library/src/form/index.php
@@ -0,0 +1,218 @@
+next_tag( 'form' );
+
+ // Get the action for this form.
+ $action = '';
+ if ( isset( $attributes['action'] ) ) {
+ $action = str_replace(
+ array( '{SITE_URL}', '{ADMIN_URL}' ),
+ array( site_url(), admin_url() ),
+ $attributes['action']
+ );
+ }
+ $processed_content->set_attribute( 'action', esc_attr( $action ) );
+
+ // Add the method attribute. If it is not set, default to `post`.
+ $method = empty( $attributes['method'] ) ? 'post' : $attributes['method'];
+ $processed_content->set_attribute( 'method', $method );
+
+ $extra_fields = apply_filters( 'render_block_core_form_extra_fields', '', $attributes );
+
+ return str_replace(
+ '',
+ $extra_fields . '',
+ $processed_content->get_updated_html()
+ );
+}
+
+/**
+ * Additional data to add to the view.js script for this block.
+ */
+function block_core_form_view_script() {
+ if ( ! gutenberg_is_experiment_enabled( 'gutenberg-form-blocks' ) ) {
+ return;
+ }
+
+ wp_localize_script(
+ 'wp-block-form-view',
+ 'wpBlockFormSettings',
+ array(
+ 'nonce' => wp_create_nonce( 'wp-block-form' ),
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'action' => 'wp_block_form_email_submit',
+ )
+ );
+}
+add_action( 'wp_enqueue_scripts', 'block_core_form_view_script' );
+
+/**
+ * Adds extra fields to the form.
+ *
+ * If the form is a comment form, adds the post ID as a hidden field,
+ * to allow the comment to be associated with the post.
+ *
+ * @param string $extra_fields The extra fields.
+ * @param array $attributes The block attributes.
+ *
+ * @return string The extra fields.
+ */
+function block_core_form_extra_fields_comment_form( $extra_fields, $attributes ) {
+ if ( ! empty( $attributes['action'] ) && str_ends_with( $attributes['action'], '/wp-comments-post.php' ) ) {
+ $extra_fields .= '';
+ }
+ return $extra_fields;
+}
+add_filter( 'render_block_core_form_extra_fields', 'block_core_form_extra_fields_comment_form', 10, 2 );
+
+/**
+ * Sends an email if the form is a contact form.
+ *
+ * @return void
+ */
+function block_core_form_send_email() {
+ check_ajax_referer( 'wp-block-form' );
+
+ // Get the POST data.
+ $params = wp_unslash( $_POST );
+ // Start building the email content.
+ $content = sprintf(
+ /* translators: %s: The request URI. */
+ __( 'Form submission from %1$s', 'gutenberg' ) . '',
+ '' . get_bloginfo( 'name' ) . ''
+ );
+
+ $skip_fields = array( 'formAction', '_ajax_nonce', 'action' );
+ foreach ( $params as $key => $value ) {
+ if ( in_array( $key, $skip_fields, true ) ) {
+ continue;
+ }
+ $content .= sanitize_key( $key ) . ': ' . wp_kses_post( $value ) . '';
+ }
+
+ // Filter the email content.
+ $content = apply_filters( 'render_block_core_form_email_content', $content, $params );
+
+ // Send the email.
+ $result = wp_mail(
+ str_replace( 'mailto:', '', $params['wp-email-address'] ),
+ __( 'Form submission', 'gutenberg' ),
+ $content
+ );
+
+ if ( ! $result ) {
+ wp_send_json_error( $result );
+ }
+ wp_send_json_success( $result );
+}
+add_action( 'wp_ajax_wp_block_form_email_submit', 'block_core_form_send_email' );
+add_action( 'wp_ajax_nopriv_wp_block_form_email_submit', 'block_core_form_send_email' );
+
+/**
+ * Send the data export/remove request if the form is a privacy-request form.
+ *
+ * @return void
+ */
+function block_core_form_privacy_form() {
+ // Get the POST data.
+ $params = wp_unslash( $_POST );
+
+ // Bail early if not a form submission, or if the nonce is not valid.
+ if ( empty( $params['wp-action'] )
+ || 'wp_privacy_send_request' !== $params['wp-action']
+ || empty( $params['wp-privacy-request'] )
+ || '1' !== $params['wp-privacy-request']
+ || empty( $params['email'] )
+ ) {
+ return;
+ }
+
+ // Get the request types.
+ $request_types = _wp_privacy_action_request_types();
+ $requests_found = array();
+ foreach ( $request_types as $request_type ) {
+ if ( ! empty( $params[ $request_type ] ) ) {
+ $requests_found[] = $request_type;
+ }
+ }
+
+ // Bail early if no requests were found.
+ if ( empty( $requests_found ) ) {
+ return;
+ }
+
+ // Process the requests.
+ $actions_errored = array();
+ $actions_performed = array();
+ foreach ( $requests_found as $action_name ) {
+ // Get the request ID.
+ $request_id = wp_create_user_request( $params['email'], $action_name );
+
+ // Bail early if the request ID is invalid.
+ if ( is_wp_error( $request_id ) ) {
+ $actions_errored[] = $action_name;
+ continue;
+ }
+
+ // Send the request email.
+ wp_send_user_request( $request_id );
+ $actions_performed[] = $action_name;
+ }
+
+ /**
+ * Determine whether the core/form-submission-notification block should be shown.
+ *
+ * @param bool $show Whether to show the core/form-submission-notification block.
+ * @param array $attributes The block attributes.
+ *
+ * @return bool Whether to show the core/form-submission-notification block.
+ */
+ $show_notification = static function ( $show, $attributes ) use ( $actions_performed, $actions_errored ) {
+ switch ( $attributes['type'] ) {
+ case 'success':
+ return ! empty( $actions_performed ) && empty( $actions_errored );
+
+ case 'error':
+ return ! empty( $actions_errored );
+
+ default:
+ return $show;
+ }
+ };
+
+ // Add filter to show the core/form-submission-notification block.
+ add_filter( 'show_form_submission_notification_block', $show_notification, 10, 2 );
+}
+add_action( 'wp', 'block_core_form_privacy_form' );
+
+/**
+ * Registers the `core/form` block on server.
+ */
+function register_block_core_form() {
+ if ( ! gutenberg_is_experiment_enabled( 'gutenberg-form-blocks' ) ) {
+ return;
+ }
+ register_block_type_from_metadata(
+ __DIR__ . '/form',
+ array(
+ 'render_callback' => 'render_block_core_form',
+ )
+ );
+}
+add_action( 'init', 'register_block_core_form' );
diff --git a/packages/block-library/src/form/init.js b/packages/block-library/src/form/init.js
new file mode 100644
index 0000000000000..79f0492c2cb2f
--- /dev/null
+++ b/packages/block-library/src/form/init.js
@@ -0,0 +1,6 @@
+/**
+ * Internal dependencies
+ */
+import { init } from './';
+
+export default init();
diff --git a/packages/block-library/src/form/save.js b/packages/block-library/src/form/save.js
new file mode 100644
index 0000000000000..a824fc076d2ac
--- /dev/null
+++ b/packages/block-library/src/form/save.js
@@ -0,0 +1,20 @@
+/**
+ * WordPress dependencies
+ */
+import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
+
+const Save = ( { attributes } ) => {
+ const blockProps = useBlockProps.save();
+ const { submissionMethod } = attributes;
+
+ return (
+
+ );
+};
+export default Save;
diff --git a/packages/block-library/src/form/utils.js b/packages/block-library/src/form/utils.js
new file mode 100644
index 0000000000000..e541f34bbc887
--- /dev/null
+++ b/packages/block-library/src/form/utils.js
@@ -0,0 +1,39 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+export const formSubmissionNotificationSuccess = [
+ 'core/form-submission-notification',
+ {
+ type: 'success',
+ },
+ [
+ [
+ 'core/paragraph',
+ {
+ content:
+ '' +
+ __( 'Your form has been submitted successfully' ) +
+ '',
+ },
+ ],
+ ],
+];
+export const formSubmissionNotificationError = [
+ 'core/form-submission-notification',
+ {
+ type: 'error',
+ },
+ [
+ [
+ 'core/paragraph',
+ {
+ content:
+ '' +
+ __( 'There was an error submitting your form.' ) +
+ '',
+ },
+ ],
+ ],
+];
diff --git a/packages/block-library/src/form/variations.js b/packages/block-library/src/form/variations.js
new file mode 100644
index 0000000000000..da3fcbbf03942
--- /dev/null
+++ b/packages/block-library/src/form/variations.js
@@ -0,0 +1,139 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+/**
+ * Internal dependencies
+ */
+import {
+ formSubmissionNotificationSuccess,
+ formSubmissionNotificationError,
+} from './utils.js';
+
+const variations = [
+ {
+ name: 'comment-form',
+ title: __( 'Experimental Comment form' ),
+ description: __( 'A comment form for posts and pages.' ),
+ attributes: {
+ submissionMethod: 'custom',
+ action: '{SITE_URL}/wp-comments-post.php',
+ method: 'post',
+ anchor: 'comment-form',
+ },
+ isDefault: false,
+ innerBlocks: [
+ [
+ 'core/form-input',
+ {
+ type: 'text',
+ name: 'author',
+ label: __( 'Name' ),
+ required: true,
+ visibilityPermissions: 'logged-out',
+ },
+ ],
+ [
+ 'core/form-input',
+ {
+ type: 'email',
+ name: 'email',
+ label: __( 'Email' ),
+ required: true,
+ visibilityPermissions: 'logged-out',
+ },
+ ],
+ [
+ 'core/form-input',
+ {
+ type: 'textarea',
+ name: 'comment',
+ label: __( 'Comment' ),
+ required: true,
+ visibilityPermissions: 'all',
+ },
+ ],
+ [ 'core/form-submit-button', {} ],
+ ],
+ scope: [ 'inserter', 'transform' ],
+ isActive: ( blockAttributes ) =>
+ ! blockAttributes?.type || blockAttributes?.type === 'text',
+ },
+ {
+ name: 'wp-privacy-form',
+ title: __( 'Experimental privacy request form' ),
+ keywords: [ 'GDPR' ],
+ description: __( 'A form torequest data exports and/or deletion.' ),
+ attributes: {
+ submissionMethod: 'custom',
+ action: '',
+ method: 'post',
+ anchor: 'gdpr-form',
+ },
+ isDefault: false,
+ innerBlocks: [
+ formSubmissionNotificationSuccess,
+ formSubmissionNotificationError,
+ [
+ 'core/paragraph',
+ {
+ content: __(
+ 'To request an export or deletion of your personal data on this site, please fill-in the form below. You can define the type of request you wish to perform, and your email address. Once the form is submitted, you will receive a confirmation email with instructions on the next steps.'
+ ),
+ },
+ ],
+ [
+ 'core/form-input',
+ {
+ type: 'email',
+ name: 'email',
+ label: __( 'Enter your email address.' ),
+ required: true,
+ visibilityPermissions: 'all',
+ },
+ ],
+ [
+ 'core/form-input',
+ {
+ type: 'checkbox',
+ name: 'export_personal_data',
+ label: __( 'Request data export' ),
+ required: false,
+ visibilityPermissions: 'all',
+ },
+ ],
+ [
+ 'core/form-input',
+ {
+ type: 'checkbox',
+ name: 'remove_personal_data',
+ label: __( 'Request data deletion' ),
+ required: false,
+ visibilityPermissions: 'all',
+ },
+ ],
+ [ 'core/form-submit-button', {} ],
+ [
+ 'core/form-input',
+ {
+ type: 'hidden',
+ name: 'wp-action',
+ value: 'wp_privacy_send_request',
+ },
+ ],
+ [
+ 'core/form-input',
+ {
+ type: 'hidden',
+ name: 'wp-privacy-request',
+ value: '1',
+ },
+ ],
+ ],
+ scope: [ 'inserter', 'transform' ],
+ isActive: ( blockAttributes ) =>
+ ! blockAttributes?.type || blockAttributes?.type === 'text',
+ },
+];
+
+export default variations;
diff --git a/packages/block-library/src/form/view.js b/packages/block-library/src/form/view.js
new file mode 100644
index 0000000000000..05efe95da545c
--- /dev/null
+++ b/packages/block-library/src/form/view.js
@@ -0,0 +1,41 @@
+// eslint-disable-next-line eslint-comments/disable-enable-pair
+/* eslint-disable no-undef */
+document.querySelectorAll( 'form.wp-block-form' ).forEach( function ( form ) {
+ // Bail If the form is not using the mailto: action.
+ if ( ! form.action || ! form.action.startsWith( 'mailto:' ) ) {
+ return;
+ }
+
+ const redirectNotification = ( status ) => {
+ const urlParams = new URLSearchParams( window.location.search );
+ urlParams.append( 'wp-form-result', status );
+ window.location.search = urlParams.toString();
+ };
+
+ // Add an event listener for the form submission.
+ form.addEventListener( 'submit', async function ( event ) {
+ event.preventDefault();
+ // Get the form data and merge it with the form action and nonce.
+ const formData = Object.fromEntries( new FormData( form ).entries() );
+ formData.formAction = form.action;
+ formData._ajax_nonce = wpBlockFormSettings.nonce;
+ formData.action = wpBlockFormSettings.action;
+
+ try {
+ const response = await fetch( wpBlockFormSettings.ajaxUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: new URLSearchParams( formData ).toString(),
+ } );
+ if ( response.ok ) {
+ redirectNotification( 'success' );
+ } else {
+ redirectNotification( 'error' );
+ }
+ } catch ( error ) {
+ redirectNotification( 'error' );
+ }
+ } );
+} );
diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js
index 736b552bf4259..e2e0fd9e414ef 100644
--- a/packages/block-library/src/index.js
+++ b/packages/block-library/src/index.js
@@ -48,6 +48,10 @@ import * as cover from './cover';
import * as details from './details';
import * as embed from './embed';
import * as file from './file';
+import * as form from './form';
+import * as formInput from './form-input';
+import * as formSubmitButton from './form-submit-button';
+import * as formSubmissionNotification from './form-submission-notification';
import * as gallery from './gallery';
import * as group from './group';
import * as heading from './heading';
@@ -228,6 +232,12 @@ const getAllBlocks = () => {
queryTitle,
postAuthorBiography,
];
+ if ( window?.__experimentalEnableFormBlocks ) {
+ blocks.push( form );
+ blocks.push( formInput );
+ blocks.push( formSubmitButton );
+ blocks.push( formSubmissionNotification );
+ }
// When in a WordPress context, conditionally
// add the classic block and TinyMCE editor
diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss
index c4b0a37e6354d..790e09535f4b6 100644
--- a/packages/block-library/src/style.scss
+++ b/packages/block-library/src/style.scss
@@ -14,6 +14,7 @@
@import "./details/style.scss";
@import "./embed/style.scss";
@import "./file/style.scss";
+@import "./form-input/style.scss";
@import "./gallery/style.scss";
@import "./group/style.scss";
@import "./heading/style.scss";
diff --git a/phpunit/bootstrap.php b/phpunit/bootstrap.php
index 7084df68443ba..acc7cfde89dbd 100644
--- a/phpunit/bootstrap.php
+++ b/phpunit/bootstrap.php
@@ -94,6 +94,7 @@ function fail_if_died( $message ) {
'gutenberg-experiments' => array(
'gutenberg-widget-experiments' => '1',
'gutenberg-full-site-editing' => 1,
+ 'gutenberg-form-blocks' => 1,
),
);
diff --git a/test/integration/fixtures/blocks/core__form-input.html b/test/integration/fixtures/blocks/core__form-input.html
new file mode 100644
index 0000000000000..718c592641bc3
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__form-input.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/test/integration/fixtures/blocks/core__form-input.json b/test/integration/fixtures/blocks/core__form-input.json
new file mode 100644
index 0000000000000..33802bbcc2088
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__form-input.json
@@ -0,0 +1,12 @@
+[
+ {
+ "name": "core/missing",
+ "isValid": true,
+ "attributes": {
+ "originalName": "core/form-input",
+ "originalUndelimitedContent": "",
+ "originalContent": "\n\n"
+ },
+ "innerBlocks": []
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__form-input.parsed.json b/test/integration/fixtures/blocks/core__form-input.parsed.json
new file mode 100644
index 0000000000000..73058fc2e17f0
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__form-input.parsed.json
@@ -0,0 +1,14 @@
+[
+ {
+ "blockName": "core/form-input",
+ "attrs": {
+ "label": "Name",
+ "required": true
+ },
+ "innerBlocks": [],
+ "innerHTML": "\n\n",
+ "innerContent": [
+ "\n\n"
+ ]
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__form-input.serialized.html b/test/integration/fixtures/blocks/core__form-input.serialized.html
new file mode 100644
index 0000000000000..4e1f6b77998de
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__form-input.serialized.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/test/integration/fixtures/blocks/core__form.html b/test/integration/fixtures/blocks/core__form.html
new file mode 100644
index 0000000000000..ab18e0e11c81a
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__form.html
@@ -0,0 +1,21 @@
+
+
+
diff --git a/test/integration/fixtures/blocks/core__form.json b/test/integration/fixtures/blocks/core__form.json
new file mode 100644
index 0000000000000..ba07b17e4d00c
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__form.json
@@ -0,0 +1,63 @@
+[
+ {
+ "name": "core/missing",
+ "isValid": true,
+ "attributes": {
+ "originalName": "core/form",
+ "originalUndelimitedContent": "",
+ "originalContent": "\n\n"
+ },
+ "innerBlocks": [
+ {
+ "name": "core/missing",
+ "isValid": true,
+ "attributes": {
+ "originalName": "core/form-input",
+ "originalUndelimitedContent": "",
+ "originalContent": "\n\n"
+ },
+ "innerBlocks": []
+ },
+ {
+ "name": "core/missing",
+ "isValid": true,
+ "attributes": {
+ "originalName": "core/form-input",
+ "originalUndelimitedContent": "",
+ "originalContent": "\n\n"
+ },
+ "innerBlocks": []
+ },
+ {
+ "name": "core/missing",
+ "isValid": true,
+ "attributes": {
+ "originalName": "core/form-input",
+ "originalUndelimitedContent": "",
+ "originalContent": "\n\n"
+ },
+ "innerBlocks": []
+ },
+ {
+ "name": "core/missing",
+ "isValid": true,
+ "attributes": {
+ "originalName": "core/form-input",
+ "originalUndelimitedContent": "",
+ "originalContent": "\n\n"
+ },
+ "innerBlocks": []
+ },
+ {
+ "name": "core/missing",
+ "isValid": true,
+ "attributes": {
+ "originalName": "core/form-input",
+ "originalUndelimitedContent": "",
+ "originalContent": "\n\n"
+ },
+ "innerBlocks": []
+ }
+ ]
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__form.parsed.json b/test/integration/fixtures/blocks/core__form.parsed.json
new file mode 100644
index 0000000000000..379bee84c84e1
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__form.parsed.json
@@ -0,0 +1,84 @@
+[
+ {
+ "blockName": "core/form",
+ "attrs": {},
+ "innerBlocks": [
+ {
+ "blockName": "core/form-input",
+ "attrs": {
+ "label": "Name",
+ "required": true
+ },
+ "innerBlocks": [],
+ "innerHTML": "\n\n",
+ "innerContent": [
+ "\n\n"
+ ]
+ },
+ {
+ "blockName": "core/form-input",
+ "attrs": {
+ "type": "email",
+ "label": "Email",
+ "required": true
+ },
+ "innerBlocks": [],
+ "innerHTML": "\n\n",
+ "innerContent": [
+ "\n\n"
+ ]
+ },
+ {
+ "blockName": "core/form-input",
+ "attrs": {
+ "type": "url",
+ "label": "Website"
+ },
+ "innerBlocks": [],
+ "innerHTML": "\n\n",
+ "innerContent": [
+ "\n\n"
+ ]
+ },
+ {
+ "blockName": "core/form-input",
+ "attrs": {
+ "type": "textarea",
+ "label": "Comment",
+ "required": true
+ },
+ "innerBlocks": [],
+ "innerHTML": "\n\n",
+ "innerContent": [
+ "\n\n"
+ ]
+ },
+ {
+ "blockName": "core/form-input",
+ "attrs": {
+ "type": "submit",
+ "label": "Submit"
+ },
+ "innerBlocks": [],
+ "innerHTML": "\n\n",
+ "innerContent": [
+ "\n\n"
+ ]
+ }
+ ],
+ "innerHTML": "\n\n",
+ "innerContent": [
+ "\n\n"
+ ]
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__form.serialized.html b/test/integration/fixtures/blocks/core__form.serialized.html
new file mode 100644
index 0000000000000..58a2a49967eb5
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__form.serialized.html
@@ -0,0 +1,19 @@
+
+
+