Repository: Automattic/jetpack
Pull request #43327: [WIP] Forms: migrate Outlined form style for inner block based fields
Author: login: ramonjd
Labels: name: [Feature] Forms
name: [Plugin] Jetpack
description: Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/
name: [Block] Form
description: Form block (also see Form package label)
name: [Package] Forms
name: [Tests] Includes Tests
Created: 2025-05-01T07:55:41Z | Merged: 2025-05-16T04:04:47Z

## Proposed changes:

Fixes https://github.com/Automattic/jetpack-roadmap/issues/2413

The main thrust of this PR is to ensure that the form style "Outlined" works as expected with inner blocks. See: https://github.com/Automattic/jetpack/pull/42890

The side quest is to fix many of the existing bugs in trunk including, but not exclusive to:

- Multi option fields do not display borders, background styles on the frontend, even though they do in the editor.
- Multi option fields are not synced with other fields when updating styles in the editor
- Global styles do not work at all.
- The select dropdown is styled, but it’s the HTML default in the frontend, and the select arrow icon takes on the input color, but it is always black in the frontend
- Thick borders interfere with the multi option blocks. You cannot add items.

With this branch you can build amazing forms like this!!

<img width="773" alt="Screenshot 2025-05-15 at 3 45 18 pm" src="https://github.com/user-attachments/assets/0182bb0c-37a6-4c40-9dbd-1b6ec3674014" />

This migration PR is the alternative to deprecating the form at https://github.com/Automattic/jetpack/pull/43366

 See JETPACK-273

Prior work:

- https://github.com/Automattic/jetpack/pull/43202
- https://github.com/Automattic/jetpack/pull/43304



## Testing instructions:

There are two objectives to this PR:

- make sure the Outline style copes with innerblocks and global styles
- tweak the styles/functionality so they're better than we left it

Given that, and the many complications and bugs in trunk already, let's not get too picky. The baseline for compatibility is that forms built in trunk shouldn't break when switching to this branch.

Insert a form with lots of fields. Preferably all of them, so let's say at least:

1. Name
2. A drop down
3. A multi checkbox
4. A multi radio
5. Textarea

Apply the "outline" style to the form.

For the `jetpack/input` and `jetpack/options` blocks, play around with border and background color styles in:

- theme.json 
- global styles (via the site editor)
- on individual blocks in the editor

Get funky with large borders and border radii.

In the editor, make sure syncing and sync toggling works (it's a toggle button at the field level)

Check the editor/frontend.

Things should look mostly the same.

https://github.com/user-attachments/assets/8ea5c3e6-6349-456a-b695-130a9af0b405


In the frontend, (not the editor) open up a few pages forms that you built in trunk previously. They shouldn't be terrible.



Diff:

Reviews:
user: login: andrewserong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:11:36Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:38:31Z
user: login: talldan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T05:18:40Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T05:48:53Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T06:28:57Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T06:29:16Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T06:45:05Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T01:15:23Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T01:23:19Z
user: login: andrewserong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T01:24:37Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T01:27:06Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T01:29:15Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T02:12:52Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T02:13:05Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T02:18:10Z
user: login: talldan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T03:10:36Z
user: login: andrewserong
body: Just left a couple of tiny PHP nits to please the linter, etc, when we're ready for polishing that side of things.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T04:23:37Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T05:19:06Z
user: login: andrewserong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T05:28:21Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T05:46:37Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T05:50:59Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T05:51:04Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T05:52:19Z
user: login: talldan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T09:43:17Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T10:32:08Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T11:04:45Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T00:39:59Z
user: login: talldan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T02:11:56Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T08:01:26Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T08:06:01Z
user: login: ramonjd
body: Testing okay so far. At least it's an improvement! 

I checked RTL too: not shabby
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T05:55:42Z
user: login: talldan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T06:39:25Z
user: login: talldan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T06:40:14Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T06:43:58Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T01:10:12Z
user: login: talldan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T03:13:24Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T03:47:06Z
user: login: ramonjd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T05:18:52Z

Review comments:
diff_hunk: @@ -0,0 +1,163 @@
+import {
+	store as blockEditorStore,
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import deepmerge from 'deepmerge';
+import { isPlainObject, isNumber } from 'lodash';
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: andrewserong
body: This might be a bit nitpicky, but since we're already using `lodash` in Jetpack, could we use lodash's `merge` rather than adding another dependency? Or is `deepmerge` needed?
created_at: 2025-05-08T02:11:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,163 @@
+import {
+	store as blockEditorStore,
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import deepmerge from 'deepmerge';
+import { isPlainObject, isNumber } from 'lodash';
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: ramonjd
body: Not at all. Good call. I was hacking here. 
created_at: 2025-05-08T02:38:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,163 @@
+import {
+	store as blockEditorStore,
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import deepmerge from 'deepmerge';
+import { isPlainObject, isNumber } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+// TODO probably an overkill.
+function mergeBaseAndUserConfigs( base, user ) {
+	if ( ! base || ! user ) {
+		return null;
+	}
+	return deepmerge( base, user, {
+		/*
+		 * We only pass as arrays the presets,
+		 * in which case we want the new array of values
+		 * to override the old array (no merging).
+		 */
+		isMergeableObject: isPlainObject,
+	} );
+}
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	innerBlockName = 'jetpack/input',
+	relativeTo = 'parent',
+	isSynced,
+} ) {
+	const formStyle = useFormStyle( clientId );
+	const { userConfig, baseConfig } = useSelect( select => {
+		const {
+			__experimentalGetCurrentGlobalStylesId,
+			getEditedEntityRecord,
+			__experimentalGetCurrentThemeBaseGlobalStyles,
+		} = select( coreStore );
+		const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+		if ( ! globalStylesId ) {
+			return null;
+		}
+
+		return {
+			userConfig: getEditedEntityRecord( 'root', 'globalStyles', globalStylesId ),
+			baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(),
+		};
+	}, [] );
+
+	const mergedGlobalBlockStyles = useMemo(
+		() => mergeBaseAndUserConfigs( baseConfig?.styles?.blocks, userConfig?.styles?.blocks ),
+		[ baseConfig?.styles?.blocks, userConfig?.styles?.blocks ]
+	);
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: talldan
body: I think the global styles stuff could be extracted into a separate hook (`useMergedBlockStyles` or something), as it's very generic.

I think this code will also need to be really well documented for the future, so that could be done as a doc block explaining why there's a need to merge these things.
created_at: 2025-05-08T03:23:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,163 @@
+import {
+	store as blockEditorStore,
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import deepmerge from 'deepmerge';
+import { isPlainObject, isNumber } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+// TODO probably an overkill.
+function mergeBaseAndUserConfigs( base, user ) {
+	if ( ! base || ! user ) {
+		return null;
+	}
+	return deepmerge( base, user, {
+		/*
+		 * We only pass as arrays the presets,
+		 * in which case we want the new array of values
+		 * to override the old array (no merging).
+		 */
+		isMergeableObject: isPlainObject,
+	} );
+}
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	innerBlockName = 'jetpack/input',
+	relativeTo = 'parent',
+	isSynced,
+} ) {
+	const formStyle = useFormStyle( clientId );
+	const { userConfig, baseConfig } = useSelect( select => {
+		const {
+			__experimentalGetCurrentGlobalStylesId,
+			getEditedEntityRecord,
+			__experimentalGetCurrentThemeBaseGlobalStyles,
+		} = select( coreStore );
+		const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+		if ( ! globalStylesId ) {
+			return null;
+		}
+
+		return {
+			userConfig: getEditedEntityRecord( 'root', 'globalStyles', globalStylesId ),
+			baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(),
+		};
+	}, [] );
+
+	const mergedGlobalBlockStyles = useMemo(
+		() => mergeBaseAndUserConfigs( baseConfig?.styles?.blocks, userConfig?.styles?.blocks ),
+		[ baseConfig?.styles?.blocks, userConfig?.styles?.blocks ]
+	);
+
+	const inputBlock = useSelect(
+		select => {
+			const { getBlock, getBlockRootClientId, getBlocksByName } = select( blockEditorStore );
+
+			let parentClientId;
+			if ( relativeTo === 'sibling' ) {
+				// Get the parent block's clientId.
+				parentClientId = getBlockRootClientId( clientId );
+				if ( ! parentClientId ) {
+					return [];
+				}
+			} else {
+				parentClientId = clientId;
+			}
+			// Get the parent block
+			const parentBlock = getBlock( parentClientId );
+			if ( ! parentBlock ) {
+				return [];
+			}
+			// if (
+			// 	isSynced &&
+			// 	( parentBlock.name === 'jetpack/field-radio' ||
+			// 		parentBlock.name === 'jetpack/field-checkbox-multiple' )
+			// ) {
+			// 	const inputs = getBlocksByName( innerBlockName );
+			// 	if ( inputs.length === 0 ) {
+			// 		return [];
+			// 	}
+			// 	console.log( 'inputs', { isSynced, input: inputs[ 0 ] } );
+			// 	return getBlock( inputs[ 0 ] );
+			// }
+
+			return parentBlock.innerBlocks.find( block => block.name === innerBlockName );
+		},
+		[ clientId, relativeTo, innerBlockName, isSynced ]
+	);
+
+	// Only return styles for outlined and animated forms.
+	if ( formStyle !== FORM_STYLE.OUTLINED && formStyle !== FORM_STYLE.ANIMATED ) {
+		return null;
+	}
+
+	// Access the input block's attributes.
+	const blockBorderClassesAndStyles = getBorderClassesAndStyles( inputBlock?.attributes ?? {} );
+	const globalBorderClassesAndStyles = getBorderClassesAndStyles( {
+		style: mergedGlobalBlockStyles?.[ innerBlockName ],
+	} );
+
+	// Notched HTML only needs the background color and associated classes.
+	const attributesWithBackgroundColor = inputBlock?.attributes
+		? {
+				backgroundColor: inputBlock?.attributes?.backgroundColor,
+				style: {
+					color: {
+						background: inputBlock?.attributes?.style?.color?.background,
+					},
+				},
+		  }
+		: {};
+	const blockColorClassesAndStyles = getColorClassesAndStyles( attributesWithBackgroundColor );
+
+	/**
+	 * Remove undefined classname values.
+	 */
+	const filteredBlockColorClassesAndStyles = [
+		blockBorderClassesAndStyles?.className,
+		blockColorClassesAndStyles?.className,
+	]
+		.filter( Boolean )
+		.join( ' ' );
+	return {
+		className: filteredBlockColorClassesAndStyles,
+		style: {
+			...blockBorderClassesAndStyles?.style,
+			// Only background here.
+			backgroundColor: blockColorClassesAndStyles?.style?.backgroundColor,
+		},
+		cssVars: {
+			// Sets the value of top: calc(var(--jetpack--contact-form--border-size) * -1) for .notched-label__label.
+			'--jetpack--contact-form--border-size':
+				getIntAsPxValue( blockBorderClassesAndStyles?.style?.borderWidth ) ||
+				blockBorderClassesAndStyles?.style?.borderTopWidth ||
+				globalBorderClassesAndStyles?.style?.borderWidth ||
+				globalBorderClassesAndStyles?.style?.borderTopWidth,
+			// Sets the value of --notch-width: max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius)); for .notched-label.
+			'--jetpack--contact-form--border-radius':
+				getIntAsPxValue( blockBorderClassesAndStyles?.style?.borderRadius ) ||
+				blockBorderClassesAndStyles?.style?.borderLeftRadius ||
+				globalBorderClassesAndStyles?.style?.borderRadius ||
+				globalBorderClassesAndStyles?.style?.borderLeftRadius,
+		},
+	};
+}
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: talldan
body: I think we need to `useMemo` all of this, I think it's going to pretty expensive in terms of perf.
created_at: 2025-05-08T03:41:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,163 @@
+import {
+	store as blockEditorStore,
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import deepmerge from 'deepmerge';
+import { isPlainObject, isNumber } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+// TODO probably an overkill.
+function mergeBaseAndUserConfigs( base, user ) {
+	if ( ! base || ! user ) {
+		return null;
+	}
+	return deepmerge( base, user, {
+		/*
+		 * We only pass as arrays the presets,
+		 * in which case we want the new array of values
+		 * to override the old array (no merging).
+		 */
+		isMergeableObject: isPlainObject,
+	} );
+}
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	innerBlockName = 'jetpack/input',
+	relativeTo = 'parent',
+	isSynced,
+} ) {
+	const formStyle = useFormStyle( clientId );
+	const { userConfig, baseConfig } = useSelect( select => {
+		const {
+			__experimentalGetCurrentGlobalStylesId,
+			getEditedEntityRecord,
+			__experimentalGetCurrentThemeBaseGlobalStyles,
+		} = select( coreStore );
+		const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+		if ( ! globalStylesId ) {
+			return null;
+		}
+
+		return {
+			userConfig: getEditedEntityRecord( 'root', 'globalStyles', globalStylesId ),
+			baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(),
+		};
+	}, [] );
+
+	const mergedGlobalBlockStyles = useMemo(
+		() => mergeBaseAndUserConfigs( baseConfig?.styles?.blocks, userConfig?.styles?.blocks ),
+		[ baseConfig?.styles?.blocks, userConfig?.styles?.blocks ]
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: talldan
body: Could this be narrowed down to a block type (`innerBlockName`), otherwise it re-runs whenever other block types we don't care about are updated. 

I think I can also see some options for improving the way this works so that hopefully we don't have to do this deep merge (instead the code could resolve individual style attributes), but I think lets focus on getting the functionality first and maybe even come back to this after the whole inner blocks stuff is merged.
created_at: 2025-05-08T03:56:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,163 @@
+import {
+	store as blockEditorStore,
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import deepmerge from 'deepmerge';
+import { isPlainObject, isNumber } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+// TODO probably an overkill.
+function mergeBaseAndUserConfigs( base, user ) {
+	if ( ! base || ! user ) {
+		return null;
+	}
+	return deepmerge( base, user, {
+		/*
+		 * We only pass as arrays the presets,
+		 * in which case we want the new array of values
+		 * to override the old array (no merging).
+		 */
+		isMergeableObject: isPlainObject,
+	} );
+}
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	innerBlockName = 'jetpack/input',
+	relativeTo = 'parent',
+	isSynced,
+} ) {
+	const formStyle = useFormStyle( clientId );
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: talldan
body: It's more of a note for later (finishing touches on the PR), but I think we should try to early return from some of the logic when outlined or animated styles are not active, just so we're not performing some potentially expensive work for no reason.
created_at: 2025-05-08T04:09:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,7 +52,7 @@ const JetpackField = props => {
 
 	return (
 		<>
-			<div { ...innerBlocksProps } />
+			<div { ...innerBlocksProps } style={ inputStyles?.cssVars } />
path: projects/packages/forms/src/blocks/shared/components/jetpack-field.js
user: login: talldan
body: `style` needs to passed to `useBlockProps`. The same thing across a few of these files. 
created_at: 2025-05-08T04:14:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,22 +62,12 @@ export default function DropdownFieldEdit( props ) {
 			templateLock: 'all',
 		}
 	);
-
-	// Note: Border styles aren't rendered on the frontend select dropdown so are
-	// omitted from the styles applied to the dropdown options wrapper here.
-	const inputColorStyles = getColorClassesAndStyles( inputBlockAttributes ?? {} );
-	const inputTypographyStyles = getTypographyClassesAndStyles( inputBlockAttributes ?? {} );
+	const inputStyles = useFormStyleOutlineClassesAndStyles( {
path: projects/packages/forms/src/blocks/field-select/edit.js
user: login: talldan
body: The way we're using both inline styles and CSS vars it feels like there's a lot of duplication.

Random idea - but I wonder if we can move away from needing to call this hook on the field wrappers at all.

It looks like we're only using this to set two css vars (and then also `--notch-width` which is derived from those), is that right?

These css vars aren't used by the field wrapper at all from what I can tell, they're only used by the label and input.

The label already calls this hook, so it knows the border values. The input also knows the border values. We could switch to setting any vars on the label and input instead.

Maybe that provides an opportunity to simplify things.
created_at: 2025-05-08T05:13:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,22 +62,12 @@ export default function DropdownFieldEdit( props ) {
 			templateLock: 'all',
 		}
 	);
-
-	// Note: Border styles aren't rendered on the frontend select dropdown so are
-	// omitted from the styles applied to the dropdown options wrapper here.
-	const inputColorStyles = getColorClassesAndStyles( inputBlockAttributes ?? {} );
-	const inputTypographyStyles = getTypographyClassesAndStyles( inputBlockAttributes ?? {} );
+	const inputStyles = useFormStyleOutlineClassesAndStyles( {
path: projects/packages/forms/src/blocks/field-select/edit.js
user: login: ramonjd
body: Thanks for looking at this.

There's a lot that can be refactored but I'm still struggling with getting 1:1 basic functionality.

From memory, we have to set the CSS vars at a higher level so child elements can use their default values, e.g., notched elements needs the border widths as well to calculate padding.

Please feel free to refactor this how you think is best - I'd appreciate the help on this one as I'm slowly losing the overview given how many rabbit holes there are.


created_at: 2025-05-08T05:48:53Z
author_association: MEMBER
diff_hunk: @@ -49,7 +52,7 @@ const JetpackField = props => {
 
 	return (
 		<>
-			<div { ...innerBlocksProps } />
+			<div { ...innerBlocksProps } style={ inputStyles?.cssVars } />
path: projects/packages/forms/src/blocks/shared/components/jetpack-field.js
user: login: ramonjd
body: Good point. Most of this is just getting it to work. 🤣 
created_at: 2025-05-08T06:28:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,155 @@
+import {
+	store as blockEditorStore,
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import { isNumber, merge } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	innerBlockName = 'jetpack/input',
+	relativeTo = 'parent',
+	isSynced,
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: ramonjd
body: Related to testing, can remove.
created_at: 2025-05-08T06:29:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,163 @@
+import {
+	store as blockEditorStore,
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import deepmerge from 'deepmerge';
+import { isPlainObject, isNumber } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+// TODO probably an overkill.
+function mergeBaseAndUserConfigs( base, user ) {
+	if ( ! base || ! user ) {
+		return null;
+	}
+	return deepmerge( base, user, {
+		/*
+		 * We only pass as arrays the presets,
+		 * in which case we want the new array of values
+		 * to override the old array (no merging).
+		 */
+		isMergeableObject: isPlainObject,
+	} );
+}
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	innerBlockName = 'jetpack/input',
+	relativeTo = 'parent',
+	isSynced,
+} ) {
+	const formStyle = useFormStyle( clientId );
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: ramonjd
body: Maybe the form style could even be passed to this hook from the block edit components. I think most have it in the `context` prop
created_at: 2025-05-08T06:45:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,137 @@
+import {
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import { isNumber, merge } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	inputBlockName,
+	inputBlockAttributes,
+} ) {
+	const formStyle = useFormStyle( clientId );
+	const { userConfig, baseConfig } = useSelect( select => {
+		const {
+			__experimentalGetCurrentGlobalStylesId,
+			getEditedEntityRecord,
+			__experimentalGetCurrentThemeBaseGlobalStyles,
+		} = select( coreStore );
+		const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+		if ( ! globalStylesId ) {
+			return null;
+		}
+
+		return {
+			userConfig: getEditedEntityRecord( 'root', 'globalStyles', globalStylesId ),
+			baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(),
+		};
+	}, [] );
+
+	const mergedGlobalBlockStyles = useMemo(
+		() =>
+			merge(
+				baseConfig?.styles?.blocks?.[ inputBlockName ],
+				userConfig?.styles?.blocks?.[ inputBlockName ]
+			),
+		[ baseConfig?.styles?.blocks, userConfig?.styles?.blocks, inputBlockName ]
+	);
+
+	return useMemo( () => {
+		// Only return styles for outlined and animated forms.
+		if ( formStyle !== FORM_STYLE.OUTLINED && formStyle !== FORM_STYLE.ANIMATED ) {
+			return null;
+		}
+		// Access the input block's attributes.
+		const blockBorderClassesAndStyles = getBorderClassesAndStyles( inputBlockAttributes ?? {} );
+		const globalBorderClassesAndStyles = getBorderClassesAndStyles( {
+			style: mergedGlobalBlockStyles?.[ inputBlockName ],
+		} );
+
+		// Notched HTML only needs the background color and associated classes.
+		const attributesWithBackgroundColor = inputBlockAttributes
+			? {
+					backgroundColor: inputBlockAttributes?.backgroundColor,
+					style: {
+						color: {
+							background: inputBlockAttributes?.style?.color?.background,
+						},
+					},
+			  }
+			: {};
+		const blockColorClassesAndStyles = getColorClassesAndStyles( attributesWithBackgroundColor );
+
+		/**
+		 * Remove undefined classname values.
+		 */
+		const filteredBlockColorClassesAndStyles = [
+			blockBorderClassesAndStyles?.className,
+			blockColorClassesAndStyles?.className,
+		]
+			.filter( Boolean )
+			.join( ' ' );
+
+		let styleSpecificCssVars = {};
+
+		if ( formStyle === FORM_STYLE.OUTLINED ) {
+			styleSpecificCssVars = {
+				'--jetpack--contact-form--notch-width':
+					'max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius))',
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: ramonjd
body: `--jetpack--contact-form--border-radius` needs to be set above to offset any increase in border radius for the notch. See the leading (left) side where border radius for the input is 11px in global styles:

<img width="719" alt="Screenshot 2025-05-09 at 11 06 48 am" src="https://github.com/user-attachments/assets/33f313e6-0fff-4084-9e0b-05021b26caa3" />

Or we could just use the raw value calculated below. I'll have a look.

created_at: 2025-05-09T01:15:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,137 @@
+import {
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import { isNumber, merge } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	inputBlockName,
+	inputBlockAttributes,
+} ) {
+	const formStyle = useFormStyle( clientId );
+	const { userConfig, baseConfig } = useSelect( select => {
+		const {
+			__experimentalGetCurrentGlobalStylesId,
+			getEditedEntityRecord,
+			__experimentalGetCurrentThemeBaseGlobalStyles,
+		} = select( coreStore );
+		const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+		if ( ! globalStylesId ) {
+			return null;
+		}
+
+		return {
+			userConfig: getEditedEntityRecord( 'root', 'globalStyles', globalStylesId ),
+			baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(),
+		};
+	}, [] );
+
+	const mergedGlobalBlockStyles = useMemo(
+		() =>
+			merge(
+				baseConfig?.styles?.blocks?.[ inputBlockName ],
+				userConfig?.styles?.blocks?.[ inputBlockName ]
+			),
+		[ baseConfig?.styles?.blocks, userConfig?.styles?.blocks, inputBlockName ]
+	);
+
+	return useMemo( () => {
+		// Only return styles for outlined and animated forms.
+		if ( formStyle !== FORM_STYLE.OUTLINED && formStyle !== FORM_STYLE.ANIMATED ) {
+			return null;
+		}
+		// Access the input block's attributes.
+		const blockBorderClassesAndStyles = getBorderClassesAndStyles( inputBlockAttributes ?? {} );
+		const globalBorderClassesAndStyles = getBorderClassesAndStyles( {
+			style: mergedGlobalBlockStyles?.[ inputBlockName ],
+		} );
+
+		// Notched HTML only needs the background color and associated classes.
+		const attributesWithBackgroundColor = inputBlockAttributes
+			? {
+					backgroundColor: inputBlockAttributes?.backgroundColor,
+					style: {
+						color: {
+							background: inputBlockAttributes?.style?.color?.background,
+						},
+					},
+			  }
+			: {};
+		const blockColorClassesAndStyles = getColorClassesAndStyles( attributesWithBackgroundColor );
+
+		/**
+		 * Remove undefined classname values.
+		 */
+		const filteredBlockColorClassesAndStyles = [
+			blockBorderClassesAndStyles?.className,
+			blockColorClassesAndStyles?.className,
+		]
+			.filter( Boolean )
+			.join( ' ' );
+
+		let styleSpecificCssVars = {};
+
+		if ( formStyle === FORM_STYLE.OUTLINED ) {
+			styleSpecificCssVars = {
+				'--jetpack--contact-form--notch-width':
+					'max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius))',
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: ramonjd
body: Actually, scrap that sorry. I think it's because of the switch from deepmerge not properly merging global styles, or something related. I'll look 👀 
created_at: 2025-05-09T01:23:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,137 @@
+import {
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import { isNumber, merge } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	inputBlockName,
+	inputBlockAttributes,
+} ) {
+	const formStyle = useFormStyle( clientId );
+	const { userConfig, baseConfig } = useSelect( select => {
+		const {
+			__experimentalGetCurrentGlobalStylesId,
+			getEditedEntityRecord,
+			__experimentalGetCurrentThemeBaseGlobalStyles,
+		} = select( coreStore );
+		const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+		if ( ! globalStylesId ) {
+			return null;
+		}
+
+		return {
+			userConfig: getEditedEntityRecord( 'root', 'globalStyles', globalStylesId ),
+			baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(),
+		};
+	}, [] );
+
+	const mergedGlobalBlockStyles = useMemo(
+		() =>
+			merge(
+				baseConfig?.styles?.blocks?.[ inputBlockName ],
+				userConfig?.styles?.blocks?.[ inputBlockName ]
+			),
+		[ baseConfig?.styles?.blocks, userConfig?.styles?.blocks, inputBlockName ]
+	);
+
+	return useMemo( () => {
+		// Only return styles for outlined and animated forms.
+		if ( formStyle !== FORM_STYLE.OUTLINED && formStyle !== FORM_STYLE.ANIMATED ) {
+			return null;
+		}
+		// Access the input block's attributes.
+		const blockBorderClassesAndStyles = getBorderClassesAndStyles( inputBlockAttributes ?? {} );
+		const globalBorderClassesAndStyles = getBorderClassesAndStyles( {
+			style: mergedGlobalBlockStyles?.[ inputBlockName ],
+		} );
+
+		// Notched HTML only needs the background color and associated classes.
+		const attributesWithBackgroundColor = inputBlockAttributes
+			? {
+					backgroundColor: inputBlockAttributes?.backgroundColor,
+					style: {
+						color: {
+							background: inputBlockAttributes?.style?.color?.background,
+						},
+					},
+			  }
+			: {};
+		const blockColorClassesAndStyles = getColorClassesAndStyles( attributesWithBackgroundColor );
+
+		/**
+		 * Remove undefined classname values.
+		 */
+		const filteredBlockColorClassesAndStyles = [
+			blockBorderClassesAndStyles?.className,
+			blockColorClassesAndStyles?.className,
+		]
+			.filter( Boolean )
+			.join( ' ' );
+
+		let styleSpecificCssVars = {};
+
+		if ( formStyle === FORM_STYLE.OUTLINED ) {
+			styleSpecificCssVars = {
+				'--jetpack--contact-form--notch-width':
+					'max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius))',
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: andrewserong
body: >  I think it's because of the switch from deepmerge not properly merging global styles

Sorry if my suggestion led you astray! Feel free to add the dependency back in if it fixes things up!
created_at: 2025-05-09T01:24:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,137 @@
+import {
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import { isNumber, merge } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	inputBlockName,
+	inputBlockAttributes,
+} ) {
+	const formStyle = useFormStyle( clientId );
+	const { userConfig, baseConfig } = useSelect( select => {
+		const {
+			__experimentalGetCurrentGlobalStylesId,
+			getEditedEntityRecord,
+			__experimentalGetCurrentThemeBaseGlobalStyles,
+		} = select( coreStore );
+		const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+		if ( ! globalStylesId ) {
+			return null;
+		}
+
+		return {
+			userConfig: getEditedEntityRecord( 'root', 'globalStyles', globalStylesId ),
+			baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(),
+		};
+	}, [] );
+
+	const mergedGlobalBlockStyles = useMemo(
+		() =>
+			merge(
+				baseConfig?.styles?.blocks?.[ inputBlockName ],
+				userConfig?.styles?.blocks?.[ inputBlockName ]
+			),
+		[ baseConfig?.styles?.blocks, userConfig?.styles?.blocks, inputBlockName ]
+	);
+
+	return useMemo( () => {
+		// Only return styles for outlined and animated forms.
+		if ( formStyle !== FORM_STYLE.OUTLINED && formStyle !== FORM_STYLE.ANIMATED ) {
+			return null;
+		}
+		// Access the input block's attributes.
+		const blockBorderClassesAndStyles = getBorderClassesAndStyles( inputBlockAttributes ?? {} );
+		const globalBorderClassesAndStyles = getBorderClassesAndStyles( {
+			style: mergedGlobalBlockStyles?.[ inputBlockName ],
+		} );
+
+		// Notched HTML only needs the background color and associated classes.
+		const attributesWithBackgroundColor = inputBlockAttributes
+			? {
+					backgroundColor: inputBlockAttributes?.backgroundColor,
+					style: {
+						color: {
+							background: inputBlockAttributes?.style?.color?.background,
+						},
+					},
+			  }
+			: {};
+		const blockColorClassesAndStyles = getColorClassesAndStyles( attributesWithBackgroundColor );
+
+		/**
+		 * Remove undefined classname values.
+		 */
+		const filteredBlockColorClassesAndStyles = [
+			blockBorderClassesAndStyles?.className,
+			blockColorClassesAndStyles?.className,
+		]
+			.filter( Boolean )
+			.join( ' ' );
+
+		let styleSpecificCssVars = {};
+
+		if ( formStyle === FORM_STYLE.OUTLINED ) {
+			styleSpecificCssVars = {
+				'--jetpack--contact-form--notch-width':
+					'max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius))',
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: ramonjd
body: No worries!! Probably an oversight on my part I'd wager.

Still convinced we could use `_.merge`... 
created_at: 2025-05-09T01:27:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,137 @@
+import {
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import { isNumber, merge } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	inputBlockName,
+	inputBlockAttributes,
+} ) {
+	const formStyle = useFormStyle( clientId );
+	const { userConfig, baseConfig } = useSelect( select => {
+		const {
+			__experimentalGetCurrentGlobalStylesId,
+			getEditedEntityRecord,
+			__experimentalGetCurrentThemeBaseGlobalStyles,
+		} = select( coreStore );
+		const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+		if ( ! globalStylesId ) {
+			return null;
+		}
+
+		return {
+			userConfig: getEditedEntityRecord( 'root', 'globalStyles', globalStylesId ),
+			baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(),
+		};
+	}, [] );
+
+	const mergedGlobalBlockStyles = useMemo(
+		() =>
+			merge(
+				baseConfig?.styles?.blocks?.[ inputBlockName ],
+				userConfig?.styles?.blocks?.[ inputBlockName ]
+			),
+		[ baseConfig?.styles?.blocks, userConfig?.styles?.blocks, inputBlockName ]
+	);
+
+	return useMemo( () => {
+		// Only return styles for outlined and animated forms.
+		if ( formStyle !== FORM_STYLE.OUTLINED && formStyle !== FORM_STYLE.ANIMATED ) {
+			return null;
+		}
+		// Access the input block's attributes.
+		const blockBorderClassesAndStyles = getBorderClassesAndStyles( inputBlockAttributes ?? {} );
+		const globalBorderClassesAndStyles = getBorderClassesAndStyles( {
+			style: mergedGlobalBlockStyles?.[ inputBlockName ],
+		} );
+
+		// Notched HTML only needs the background color and associated classes.
+		const attributesWithBackgroundColor = inputBlockAttributes
+			? {
+					backgroundColor: inputBlockAttributes?.backgroundColor,
+					style: {
+						color: {
+							background: inputBlockAttributes?.style?.color?.background,
+						},
+					},
+			  }
+			: {};
+		const blockColorClassesAndStyles = getColorClassesAndStyles( attributesWithBackgroundColor );
+
+		/**
+		 * Remove undefined classname values.
+		 */
+		const filteredBlockColorClassesAndStyles = [
+			blockBorderClassesAndStyles?.className,
+			blockColorClassesAndStyles?.className,
+		]
+			.filter( Boolean )
+			.join( ' ' );
+
+		let styleSpecificCssVars = {};
+
+		if ( formStyle === FORM_STYLE.OUTLINED ) {
+			styleSpecificCssVars = {
+				'--jetpack--contact-form--notch-width':
+					'max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius))',
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: ramonjd
body: Yep, totally my fault 🤣 Fixing.
created_at: 2025-05-09T01:29:15Z
author_association: MEMBER
diff_hunk: @@ -17,21 +18,53 @@ const getLabelOrFallback = ( label, placeholder ) => {
 	return label ?? placeholder;
 };
 
-const WithNotchedWrapper = ( { formStyle, children } ) => {
+const OPTIONS_FIELDS = [ 'jetpack/field-single-choice', 'jetpack/field-multiple-choice' ];
path: projects/packages/forms/src/blocks/label/edit.js
user: login: ramonjd
body: ```suggestion
const OPTIONS_FIELDS = [ 'jetpack/field-radio', 'jetpack/field-checkbox-multiple' ];
```

For some reason the folder names differ from the block names.
created_at: 2025-05-09T02:12:52Z
author_association: MEMBER
diff_hunk: @@ -17,21 +18,53 @@ const getLabelOrFallback = ( label, placeholder ) => {
 	return label ?? placeholder;
 };
 
-const WithNotchedWrapper = ( { formStyle, children } ) => {
+const OPTIONS_FIELDS = [ 'jetpack/field-single-choice', 'jetpack/field-multiple-choice' ];
+
+function useSiblingBlock( clientId ) {
+	const inputBlock = useSelect(
+		select => {
+			const { getBlock, getBlockRootClientId } = select( blockEditorStore );
+
+			// Get the parent block's clientId.
+			const parentClientId = getBlockRootClientId( clientId );
+			if ( ! parentClientId ) {
+				return {};
+			}
+			// Get the parent block
+			const parentBlock = getBlock( parentClientId );
+			if ( ! parentBlock ) {
+				return {};
+			}
+
+			const siblingBlockType = OPTIONS_FIELDS.includes( parentBlock.name )
+				? 'jetpack/option'
path: projects/packages/forms/src/blocks/label/edit.js
user: login: ramonjd
body: ```suggestion
				? 'jetpack/options'
```
created_at: 2025-05-09T02:13:05Z
author_association: MEMBER
diff_hunk: @@ -31,12 +32,16 @@ const getInputClass = type => {
 const InputEdit = ( { attributes, clientId, isSelected, name, setAttributes, context } ) => {
 	const { 'jetpack/field-share-attributes': isSynced } = context;
 	useSyncedAttributes( name, isSynced, SYNCED_ATTRIBUTE_KEYS, attributes, setAttributes );
-
 	const { max, min, placeholder, type } = attributes;
+	const styles = useFormStyleOutlineClassesAndStyles( {
+		clientId,
+		inputBlockName: name,
+		inputBlockAttributes: attributes,
+	} );
path: projects/packages/forms/src/blocks/input/edit.js
user: login: ramonjd
body: Nice! I've just been toying with multiple options. 

I think they need a variation of the new `useSiblingBlock` hook in label, but to get the attributes of the `jetpack/options` field. E.g., 

```js
function useInnerOptionsBlock( clientId ) {
	const inputBlock = useSelect(
		select => {
			// Get the parent block
			const parentBlock = select( blockEditorStore ).getBlock( clientId );
			if ( ! parentBlock ) {
				return {};
			}

			return parentBlock.innerBlocks.find( block => block.name === 'jetpack/options' );
		},
		[ clientId ]
	);

	return inputBlock;
}
```

Looking at this now.

created_at: 2025-05-09T02:18:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,136 @@
+import {
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import { isNumber, merge } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	inputBlockName,
+	inputBlockAttributes,
+} ) {
+	const formStyle = useFormStyle( clientId );
+	const { userConfig, baseConfig } = useSelect( select => {
+		const {
+			__experimentalGetCurrentGlobalStylesId,
+			getEditedEntityRecord,
+			__experimentalGetCurrentThemeBaseGlobalStyles,
+		} = select( coreStore );
+		const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+		if ( ! globalStylesId ) {
+			return null;
+		}
+
+		return {
+			userConfig: getEditedEntityRecord( 'root', 'globalStyles', globalStylesId ),
+			baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(),
+		};
+	}, [] );
+
+	const mergedGlobalBlockStyles = useMemo(
+		() =>
+			merge(
+				baseConfig?.styles?.blocks?.[ inputBlockName ],
+				userConfig?.styles?.blocks?.[ inputBlockName ]
+			),
+		[ baseConfig?.styles?.blocks, userConfig?.styles?.blocks, inputBlockName ]
+	);
+
+	return useMemo( () => {
+		// Only return styles for outlined and animated forms.
+		if ( formStyle !== FORM_STYLE.OUTLINED && formStyle !== FORM_STYLE.ANIMATED ) {
+			return null;
+		}
+		// Access the input block's attributes.
+		const blockBorderClassesAndStyles = getBorderClassesAndStyles( inputBlockAttributes ?? {} );
+		const globalBorderClassesAndStyles = getBorderClassesAndStyles( {
+			style: mergedGlobalBlockStyles,
+		} );
+
+		// Notched HTML only needs the background color and associated classes.
+		const attributesWithBackgroundColor = inputBlockAttributes
+			? {
+					backgroundColor: inputBlockAttributes?.backgroundColor,
+					style: {
+						color: {
+							background: inputBlockAttributes?.style?.color?.background,
+						},
+					},
+			  }
+			: {};
+		const blockColorClassesAndStyles = getColorClassesAndStyles( attributesWithBackgroundColor );
+
+		/**
+		 * Remove undefined classname values.
+		 */
+		const filteredBlockColorClassesAndStyles = [
+			blockBorderClassesAndStyles?.className,
+			blockColorClassesAndStyles?.className,
+		]
+			.filter( Boolean )
+			.join( ' ' );
+
+		let styleSpecificCssVars = {};
+
+		if ( formStyle === FORM_STYLE.OUTLINED ) {
+			styleSpecificCssVars = {
+				'--jetpack--contact-form--notch-width':
+					'max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius))',
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: talldan
body: I think it's also a problem in trunk, but we should probably cap this value somehow. Users can set a really high value for border radius, and it makes the leading notch element will just keep increasing in size.
created_at: 2025-05-09T03:10:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1391,23 +1402,80 @@ public function render_outline_label( $id, $label, $required, $required_field_te
 		$classes .= $this->is_error() ? ' form-error' : '';
 		$classes .= $this->label_classes ? ' ' . $this->label_classes : '';
 
+		$output_data = $this->get_outline_styles();
+
 		return '
 			<div class="notched-label">
-				<div class="notched-label__leading"></div>
-				<div class="notched-label__notch">
+				<div class="notched-label__leading ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
+				<div class="notched-label__notch ' . $output_data['class_name'] . '"' . $output_data['style'] . '>
 					<label
 						for="' . esc_attr( $id ) . '"
 						class=" ' . $classes . '"
-						style="' . $this->label_styles . '"
+						style="' . $this->label_styles . $output_data['css_vars'] . '"
 					>'
 			. esc_html( $label )
 			. ( $required ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
 			'</label>
 				</div>
-				<div class="notched-label__trailing"></div>
+				<div class="notched-label__trailing ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: andrewserong
body: Will we need to `esc_attr` each of these?
created_at: 2025-05-09T04:13:30Z
author_association: MEMBER
diff_hunk: @@ -1391,23 +1402,80 @@ public function render_outline_label( $id, $label, $required, $required_field_te
 		$classes .= $this->is_error() ? ' form-error' : '';
 		$classes .= $this->label_classes ? ' ' . $this->label_classes : '';
 
+		$output_data = $this->get_outline_styles();
+
 		return '
 			<div class="notched-label">
-				<div class="notched-label__leading"></div>
-				<div class="notched-label__notch">
+				<div class="notched-label__leading ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
+				<div class="notched-label__notch ' . $output_data['class_name'] . '"' . $output_data['style'] . '>
 					<label
 						for="' . esc_attr( $id ) . '"
 						class=" ' . $classes . '"
-						style="' . $this->label_styles . '"
+						style="' . $this->label_styles . $output_data['css_vars'] . '"
 					>'
 			. esc_html( $label )
 			. ( $required ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
 			'</label>
 				</div>
-				<div class="notched-label__trailing"></div>
+				<div class="notched-label__trailing ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
 			</div>';
 	}
 
+	/**
+	 * Returns the styles, classes and CSS variables associated with the outline style.
+	 *
+	 * @return array {
+	 *     @type string $style_attrs The style attributes.
+	 *     @type string $css_vars The CSS variables.
+	 *     @type string $class_name The class name.
+	 * }
+	 */
+	private function get_outline_styles() {
+		$style_attrs     = '';
+		$css_vars        = '';
+		$outline_styles  = $this->get_attribute( 'outlinestyledata' );
+		$outline_classes = $this->get_attribute( 'outlinestyleclasses' );
+
+		if ( ! empty( $outline_styles ) ) {
+			$outline_styles = json_decode( html_entity_decode( $outline_styles, ENT_COMPAT ), true );
+			$input_styles   = $this->get_attribute( 'inputstyles' );
+
+			if ( ! empty( $input_styles ) ) {
+				$style_attrs = ' style="' . esc_attr( $input_styles ) . '"';
+			}
+
+			$global_styles = wp_get_global_styles(
+				array( 'border' ),
+				array(
+					'block_name' => 'jetpack/input',
+					'transforms' => array( 'resolve-variables' ),
+				)
+			);
+
+			$legacy_border_size   = ( isset( $outline_styles['width'] ) && is_numeric( $outline_styles['width'] ) ) ? $outline_styles['width'] . 'px' : $outline_styles['width'];
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: andrewserong
body: The linter is complaining about this line and the one below because the keys `width` and `border` might not exist. Could something like the following work instead?

```php
			$legacy_border_size   = $outline_styles['width'] ?? null;
			$legacy_border_size   = is_numeric( $legacy_border_size ) ? $legacy_border_size . 'px' : $legacy_border_size;
			$legacy_border_radius = $outline_styles['radius'] ?? null;
			$legacy_border_radius = is_numeric( $legacy_border_radius ) ? $legacy_border_radius . 'px' : $legacy_border_radius;
```


created_at: 2025-05-09T04:21:08Z
author_association: MEMBER
diff_hunk: @@ -1391,23 +1402,80 @@ public function render_outline_label( $id, $label, $required, $required_field_te
 		$classes .= $this->is_error() ? ' form-error' : '';
 		$classes .= $this->label_classes ? ' ' . $this->label_classes : '';
 
+		$output_data = $this->get_outline_styles();
+
 		return '
 			<div class="notched-label">
-				<div class="notched-label__leading"></div>
-				<div class="notched-label__notch">
+				<div class="notched-label__leading ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
+				<div class="notched-label__notch ' . $output_data['class_name'] . '"' . $output_data['style'] . '>
 					<label
 						for="' . esc_attr( $id ) . '"
 						class=" ' . $classes . '"
-						style="' . $this->label_styles . '"
+						style="' . $this->label_styles . $output_data['css_vars'] . '"
 					>'
 			. esc_html( $label )
 			. ( $required ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
 			'</label>
 				</div>
-				<div class="notched-label__trailing"></div>
+				<div class="notched-label__trailing ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
 			</div>';
 	}
 
+	/**
+	 * Returns the styles, classes and CSS variables associated with the outline style.
+	 *
+	 * @return array {
+	 *     @type string $style_attrs The style attributes.
+	 *     @type string $css_vars The CSS variables.
+	 *     @type string $class_name The class name.
+	 * }
+	 */
+	private function get_outline_styles() {
+		$style_attrs     = '';
+		$css_vars        = '';
+		$outline_styles  = $this->get_attribute( 'outlinestyledata' );
+		$outline_classes = $this->get_attribute( 'outlinestyleclasses' );
+
+		if ( ! empty( $outline_styles ) ) {
+			$outline_styles = json_decode( html_entity_decode( $outline_styles, ENT_COMPAT ), true );
+			$input_styles   = $this->get_attribute( 'inputstyles' );
+
+			if ( ! empty( $input_styles ) ) {
+				$style_attrs = ' style="' . esc_attr( $input_styles ) . '"';
+			}
+
+			$global_styles = wp_get_global_styles(
+				array( 'border' ),
+				array(
+					'block_name' => 'jetpack/input',
+					'transforms' => array( 'resolve-variables' ),
+				)
+			);
+
+			$legacy_border_size   = ( isset( $outline_styles['width'] ) && is_numeric( $outline_styles['width'] ) ) ? $outline_styles['width'] . 'px' : $outline_styles['width'];
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: ramonjd
body: Strange. The linter can't recognise the first `isset( $outline_styles['width'] )`?
created_at: 2025-05-09T05:19:06Z
author_association: MEMBER
diff_hunk: @@ -1391,23 +1402,80 @@ public function render_outline_label( $id, $label, $required, $required_field_te
 		$classes .= $this->is_error() ? ' form-error' : '';
 		$classes .= $this->label_classes ? ' ' . $this->label_classes : '';
 
+		$output_data = $this->get_outline_styles();
+
 		return '
 			<div class="notched-label">
-				<div class="notched-label__leading"></div>
-				<div class="notched-label__notch">
+				<div class="notched-label__leading ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
+				<div class="notched-label__notch ' . $output_data['class_name'] . '"' . $output_data['style'] . '>
 					<label
 						for="' . esc_attr( $id ) . '"
 						class=" ' . $classes . '"
-						style="' . $this->label_styles . '"
+						style="' . $this->label_styles . $output_data['css_vars'] . '"
 					>'
 			. esc_html( $label )
 			. ( $required ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
 			'</label>
 				</div>
-				<div class="notched-label__trailing"></div>
+				<div class="notched-label__trailing ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
 			</div>';
 	}
 
+	/**
+	 * Returns the styles, classes and CSS variables associated with the outline style.
+	 *
+	 * @return array {
+	 *     @type string $style_attrs The style attributes.
+	 *     @type string $css_vars The CSS variables.
+	 *     @type string $class_name The class name.
+	 * }
+	 */
+	private function get_outline_styles() {
+		$style_attrs     = '';
+		$css_vars        = '';
+		$outline_styles  = $this->get_attribute( 'outlinestyledata' );
+		$outline_classes = $this->get_attribute( 'outlinestyleclasses' );
+
+		if ( ! empty( $outline_styles ) ) {
+			$outline_styles = json_decode( html_entity_decode( $outline_styles, ENT_COMPAT ), true );
+			$input_styles   = $this->get_attribute( 'inputstyles' );
+
+			if ( ! empty( $input_styles ) ) {
+				$style_attrs = ' style="' . esc_attr( $input_styles ) . '"';
+			}
+
+			$global_styles = wp_get_global_styles(
+				array( 'border' ),
+				array(
+					'block_name' => 'jetpack/input',
+					'transforms' => array( 'resolve-variables' ),
+				)
+			);
+
+			$legacy_border_size   = ( isset( $outline_styles['width'] ) && is_numeric( $outline_styles['width'] ) ) ? $outline_styles['width'] . 'px' : $outline_styles['width'];
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: andrewserong
body: No, I think the problem is that the second operand of the ternary falls back to `$outline_styles['width']` which sits outside of the `( isset( $outline_styles['width'] ) && is_numeric( $outline_styles['width'] ) )` check. I.e. if that evaluates to `false` we attempt to grab `['width']` anyway, even though it might not be set.
created_at: 2025-05-09T05:28:21Z
author_association: MEMBER
diff_hunk: @@ -1391,23 +1402,80 @@ public function render_outline_label( $id, $label, $required, $required_field_te
 		$classes .= $this->is_error() ? ' form-error' : '';
 		$classes .= $this->label_classes ? ' ' . $this->label_classes : '';
 
+		$output_data = $this->get_outline_styles();
+
 		return '
 			<div class="notched-label">
-				<div class="notched-label__leading"></div>
-				<div class="notched-label__notch">
+				<div class="notched-label__leading ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
+				<div class="notched-label__notch ' . $output_data['class_name'] . '"' . $output_data['style'] . '>
 					<label
 						for="' . esc_attr( $id ) . '"
 						class=" ' . $classes . '"
-						style="' . $this->label_styles . '"
+						style="' . $this->label_styles . $output_data['css_vars'] . '"
 					>'
 			. esc_html( $label )
 			. ( $required ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
 			'</label>
 				</div>
-				<div class="notched-label__trailing"></div>
+				<div class="notched-label__trailing ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
 			</div>';
 	}
 
+	/**
+	 * Returns the styles, classes and CSS variables associated with the outline style.
+	 *
+	 * @return array {
+	 *     @type string $style_attrs The style attributes.
+	 *     @type string $css_vars The CSS variables.
+	 *     @type string $class_name The class name.
+	 * }
+	 */
+	private function get_outline_styles() {
+		$style_attrs     = '';
+		$css_vars        = '';
+		$outline_styles  = $this->get_attribute( 'outlinestyledata' );
+		$outline_classes = $this->get_attribute( 'outlinestyleclasses' );
+
+		if ( ! empty( $outline_styles ) ) {
+			$outline_styles = json_decode( html_entity_decode( $outline_styles, ENT_COMPAT ), true );
+			$input_styles   = $this->get_attribute( 'inputstyles' );
+
+			if ( ! empty( $input_styles ) ) {
+				$style_attrs = ' style="' . esc_attr( $input_styles ) . '"';
+			}
+
+			$global_styles = wp_get_global_styles(
+				array( 'border' ),
+				array(
+					'block_name' => 'jetpack/input',
+					'transforms' => array( 'resolve-variables' ),
+				)
+			);
+
+			$legacy_border_size   = ( isset( $outline_styles['width'] ) && is_numeric( $outline_styles['width'] ) ) ? $outline_styles['width'] . 'px' : $outline_styles['width'];
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: ramonjd
body: Oh face palm. I didn't scroll horizontally. Good one!
created_at: 2025-05-09T05:46:37Z
author_association: MEMBER
diff_hunk: @@ -1391,23 +1402,80 @@ public function render_outline_label( $id, $label, $required, $required_field_te
 		$classes .= $this->is_error() ? ' form-error' : '';
 		$classes .= $this->label_classes ? ' ' . $this->label_classes : '';
 
+		$output_data = $this->get_outline_styles();
+
 		return '
 			<div class="notched-label">
-				<div class="notched-label__leading"></div>
-				<div class="notched-label__notch">
+				<div class="notched-label__leading ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
+				<div class="notched-label__notch ' . $output_data['class_name'] . '"' . $output_data['style'] . '>
 					<label
 						for="' . esc_attr( $id ) . '"
 						class=" ' . $classes . '"
-						style="' . $this->label_styles . '"
+						style="' . $this->label_styles . $output_data['css_vars'] . '"
 					>'
 			. esc_html( $label )
 			. ( $required ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
 			'</label>
 				</div>
-				<div class="notched-label__trailing"></div>
+				<div class="notched-label__trailing ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
 			</div>';
 	}
 
+	/**
+	 * Returns the styles, classes and CSS variables associated with the outline style.
+	 *
+	 * @return array {
+	 *     @type string $style_attrs The style attributes.
+	 *     @type string $css_vars The CSS variables.
+	 *     @type string $class_name The class name.
+	 * }
+	 */
+	private function get_outline_styles() {
+		$style_attrs     = '';
+		$css_vars        = '';
+		$outline_styles  = $this->get_attribute( 'outlinestyledata' );
+		$outline_classes = $this->get_attribute( 'outlinestyleclasses' );
+
+		if ( ! empty( $outline_styles ) ) {
+			$outline_styles = json_decode( html_entity_decode( $outline_styles, ENT_COMPAT ), true );
+			$input_styles   = $this->get_attribute( 'inputstyles' );
+
+			if ( ! empty( $input_styles ) ) {
+				$style_attrs = ' style="' . esc_attr( $input_styles ) . '"';
+			}
+
+			$global_styles = wp_get_global_styles(
+				array( 'border' ),
+				array(
+					'block_name' => 'jetpack/input',
+					'transforms' => array( 'resolve-variables' ),
+				)
+			);
+
+			$legacy_border_size   = ( isset( $outline_styles['width'] ) && is_numeric( $outline_styles['width'] ) ) ? $outline_styles['width'] . 'px' : $outline_styles['width'];
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: ramonjd
body: Done, thanks for the example code too. 🙇🏻 
created_at: 2025-05-09T05:50:59Z
author_association: MEMBER
diff_hunk: @@ -1391,23 +1402,80 @@ public function render_outline_label( $id, $label, $required, $required_field_te
 		$classes .= $this->is_error() ? ' form-error' : '';
 		$classes .= $this->label_classes ? ' ' . $this->label_classes : '';
 
+		$output_data = $this->get_outline_styles();
+
 		return '
 			<div class="notched-label">
-				<div class="notched-label__leading"></div>
-				<div class="notched-label__notch">
+				<div class="notched-label__leading ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
+				<div class="notched-label__notch ' . $output_data['class_name'] . '"' . $output_data['style'] . '>
 					<label
 						for="' . esc_attr( $id ) . '"
 						class=" ' . $classes . '"
-						style="' . $this->label_styles . '"
+						style="' . $this->label_styles . $output_data['css_vars'] . '"
 					>'
 			. esc_html( $label )
 			. ( $required ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
 			'</label>
 				</div>
-				<div class="notched-label__trailing"></div>
+				<div class="notched-label__trailing ' . $output_data['class_name'] . '"' . $output_data['style'] . '></div>
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: ramonjd
body: Done!
created_at: 2025-05-09T05:51:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,19 @@
+import { store as blockEditorStore } from '@wordpress/block-editor';
+import { useSelect } from '@wordpress/data';
+
+export default function useGetInnerBlock( clientId, blockName ) {
path: projects/packages/forms/src/blocks/shared/hooks/use-get-inner-block.js
user: login: ramonjd
body: There's a path to combine this with [useSibling](https://github.com/Automattic/jetpack/pull/43327/files#diff-69c941a5982946d2b26ef728f14dba96e841e0b1d2a88746cdf73841c36f82abR23), but I left it in case we roll back these changes.
created_at: 2025-05-09T05:52:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,19 @@
+import { store as blockEditorStore } from '@wordpress/block-editor';
+import { useSelect } from '@wordpress/data';
+
+export default function useGetInnerBlock( clientId, blockName ) {
path: projects/packages/forms/src/blocks/shared/hooks/use-get-inner-block.js
user: login: talldan
body: The latest code seems to be going back to the old approach of setting css values on field blocks, which I'd moved away from, so I'll have a look at if we can remove it.

I think we need to have every field working the same, as otherwise it'll be too challenging to keep up with all the inconsistencies.
created_at: 2025-05-09T09:43:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { store as blockEditorStore } from '@wordpress/block-editor';
+import { useSelect } from '@wordpress/data';
+
+export default function useGetInnerBlock( clientId, blockName ) {
path: projects/packages/forms/src/blocks/shared/hooks/use-get-inner-block.js
user: login: ramonjd
body: Ah yeah, fair enough. Wasn't thinking. I'll see if they can be added to the options block itself. 
created_at: 2025-05-09T10:32:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,19 @@
+import { store as blockEditorStore } from '@wordpress/block-editor';
+import { useSelect } from '@wordpress/data';
+
+export default function useGetInnerBlock( clientId, blockName ) {
path: projects/packages/forms/src/blocks/shared/hooks/use-get-inner-block.js
user: login: ramonjd
body: Done in: a153a80eba7699a31668ae80ae5c0e2962123416

They're doubled up by necessity (on label and options blocks) since the options needs to calculate inner padding. Same as inputs I see. I think my brain was trying to avoid the duplication. I dunno 😄 

**TODO** on Monday: ensure the CSS vars are applied to radio/checkboxes on the frontend. (They're not)
created_at: 2025-05-09T11:04:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,163 @@
+import {
+	store as blockEditorStore,
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import deepmerge from 'deepmerge';
+import { isPlainObject, isNumber } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+// TODO probably an overkill.
+function mergeBaseAndUserConfigs( base, user ) {
+	if ( ! base || ! user ) {
+		return null;
+	}
+	return deepmerge( base, user, {
+		/*
+		 * We only pass as arrays the presets,
+		 * in which case we want the new array of values
+		 * to override the old array (no merging).
+		 */
+		isMergeableObject: isPlainObject,
+	} );
+}
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+export default function useFormStyleOutlineClassesAndStyles( {
+	clientId,
+	innerBlockName = 'jetpack/input',
+	relativeTo = 'parent',
+	isSynced,
+} ) {
+	const formStyle = useFormStyle( clientId );
+	const { userConfig, baseConfig } = useSelect( select => {
+		const {
+			__experimentalGetCurrentGlobalStylesId,
+			getEditedEntityRecord,
+			__experimentalGetCurrentThemeBaseGlobalStyles,
+		} = select( coreStore );
+		const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+		if ( ! globalStylesId ) {
+			return null;
+		}
+
+		return {
+			userConfig: getEditedEntityRecord( 'root', 'globalStyles', globalStylesId ),
+			baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(),
+		};
+	}, [] );
+
+	const mergedGlobalBlockStyles = useMemo(
+		() => mergeBaseAndUserConfigs( baseConfig?.styles?.blocks, userConfig?.styles?.blocks ),
+		[ baseConfig?.styles?.blocks, userConfig?.styles?.blocks ]
path: projects/packages/forms/src/blocks/shared/hooks/use-form-style-outline-classes-and-styles.js
user: login: ramonjd
body: Done!
created_at: 2025-05-12T00:39:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,19 @@
+import { store as blockEditorStore } from '@wordpress/block-editor';
+import { useSelect } from '@wordpress/data';
+
+export default function useGetInnerBlock( clientId, blockName ) {
path: projects/packages/forms/src/blocks/shared/hooks/use-get-inner-block.js
user: login: talldan
body: > Same as inputs I see. I think my brain was trying to avoid the duplication.

My thinking is it still ends up being duplication whether on the field or input/options blocks, but at least on the input/options blocks the attributes we need are the ones from the block itself, so it saves a step.
created_at: 2025-05-12T02:11:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -799,9 +811,26 @@ public function render_textarea_field( $id, $label, $value, $class, $required, $
 	 * @return string HTML
 	 */
 	public function render_radio_field( $id, $label, $value, $class, $required, $required_field_text ) {
-		$field  = '<fieldset id="' . esc_attr( "$id-label" ) . '" class="grunion-radio-options">';
+		$options_classes   = $this->get_attribute( 'optionsclasses' );
+		$options_styles    = $this->get_attribute( 'optionsstyles' );
+		$form_style        = $this->get_form_style();
+		$is_animated_style = 'animated' === $form_style;
+		$fieldset_id       = "id='" . esc_attr( "$id-label" ) . "'";
+
+		if ( $is_animated_style ) {
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: ramonjd
body: Pretty dodgy. Either this, or add the notched HTML to animated style multi fields in the editor. The less of that the better I guessed.

TODO: test this in trunk-created forms. Might need to gate it for inner block forms only.
created_at: 2025-05-12T08:01:26Z
author_association: MEMBER
diff_hunk: @@ -316,7 +316,7 @@ public static function parse( $attributes, $content ) {
 			// Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
 			// (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
 			// when WordPress does the real loop.
-			wp_enqueue_style( 'grunion.css' );
+			wp_enqueue_style( 'grunion.css?123' );
path: projects/packages/forms/src/contact-form/class-contact-form.php
user: login: ramonjd
body: I'll get rid of this. Was just to cache bust frontend CSS changes
created_at: 2025-05-12T08:06:01Z
author_association: MEMBER
diff_hunk: @@ -4,7 +4,7 @@ import { useDispatch, useSelect } from '@wordpress/data';
 import { useEffect } from '@wordpress/element';
 import { __ } from '@wordpress/i18n';
 
-const FORM_BLOCK_NAME = 'jetpack/contact-form';
+export const FORM_BLOCK_NAME = 'jetpack/contact-form';
path: projects/packages/forms/src/blocks/contact-form/util/form.js
user: login: ramonjd
body: This export is not required as far as I can see.
created_at: 2025-05-15T05:50:20Z
author_association: MEMBER
diff_hunk: @@ -39,7 +39,6 @@ export default function DateFieldEdit( props ) {
 		template,
 		templateLock: 'all',
 	} );
-
path: projects/packages/forms/src/blocks/field-date/edit.js
user: login: ramonjd
body: We can roll back this change.


created_at: 2025-05-15T05:50:45Z
author_association: MEMBER
diff_hunk: @@ -1151,13 +1221,59 @@ private function get_unauth_endpoint_url() {
 	 * @return string HTML
 	 */
 	public function render_checkbox_multiple_field( $id, $label, $value, $class, $required, $required_field_text ) {
+		$options_classes   = $this->get_attribute( 'optionsclasses' );
+		$options_styles    = $this->get_attribute( 'optionsstyles' );
+		$form_style        = $this->get_form_style();
+		$is_outlined_style = 'outlined' === $form_style;
+
 		// The `data-required` attribute is used in `accessible-form.js` to ensure at least one
 		// checkbox is checked. Unlike radio buttons, for which the required attribute is satisfied if
 		// any of the radio buttons in the group is selected, adding a required attribute directly to
 		// a checkbox means that this specific checkbox must be checked.
-		$field  = '<fieldset id="' . esc_attr( "$id-label" ) . '" class="grunion-checkbox-multiple-options"' . ( $required ? ' data-required' : '' ) . '>';
+
+		$fieldset_id = "id='" . esc_attr( "$id-label" ) . "'";
+
+		$outline_border_radius = '';
+		if ( $is_outlined_style ) {
+			$outline_styles = $this->get_attribute( 'outlinestyledata' );
+
+			if ( ! empty( $outline_styles ) ) {
+				$outline_styles = json_decode( html_entity_decode( $outline_styles, ENT_COMPAT ), true );
+			}
+
+			// When there's an outlined style, and border radius is set, we override the inline border radius to apply
+			// a limit of `100px` to the radius on the x axis. This achieves the same look and feel as other fields
+			// that use the notch html (`notched-label__leading` has a max-width of `100px` to prevent it from getting too wide).
+			if ( isset( $outline_styles['border']['radius'] ) ) {
+				$radius                  = $outline_styles['border']['radius'];
+				$has_split_radius_values = is_array( $radius );
+				$top_left_radius         = $has_split_radius_values ? $radius['topLeft'] : $radius;
+				$top_right_radius        = $has_split_radius_values ? $radius['topRight'] : $radius;
+				$bottom_left_radius      = $has_split_radius_values ? $radius['bottomLeft'] : $radius;
+				$bottom_right_radius     = $has_split_radius_values ? $radius['bottomRight'] : $radius;
+				$outline_border_radius  .= "border-top-left-radius: min(100px, {$top_left_radius}) {$top_left_radius};";
+				$outline_border_radius  .= "border-top-right-radius: min(100px, {$top_right_radius}) {$top_right_radius};";
+				$outline_border_radius  .= "border-bottom-left-radius: min(100px, {$bottom_left_radius}) {$bottom_left_radius};";
+				$outline_border_radius  .= "border-bottom-right-radius: min(100px, {$bottom_right_radius}) {$bottom_right_radius};";
+
+			}
+		}
+
+		/*
+		 * For the "outlined" style, the styles and classes are applied to the fieldset element.
+		 */
+		if ( $is_outlined_style ) {
+			$field = "<fieldset {$fieldset_id} class='wp-block-jetpack-options grunion-checkbox-multiple-options" . $options_classes . "' style='" . $options_styles . $outline_border_radius . "' " . ( $required ? 'data-required' : '' ) . '>';
+		} else {
+			$field = "<fieldset {$fieldset_id} class='jetpack-field-multiple__fieldset'>";
+		}
+
 		$field .= $this->render_legend_as_label( '', $id, $label, $required, $required_field_text );
 
+		if ( ! $is_outlined_style ) {
+			$field .= "<div class='wp-block-jetpack-options grunion-checkbox-multiple-options" . $options_classes . "' style='" . $options_styles . "' " . ( $required ? 'data-required' : '' ) . '>';
+		}
+
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: ramonjd
body: We probably need to document all this to some extent. 
created_at: 2025-05-15T05:53:49Z
author_association: MEMBER
diff_hunk: @@ -1391,23 +1509,111 @@ public function render_outline_label( $id, $label, $required, $required_field_te
 		$classes .= $this->is_error() ? ' form-error' : '';
 		$classes .= $this->label_classes ? ' ' . $this->label_classes : '';
 
+		$output_data = $this->get_outline_styles();
+
 		return '
 			<div class="notched-label">
-				<div class="notched-label__leading"></div>
-				<div class="notched-label__notch">
+				<div class="notched-label__leading ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
+				<div class="notched-label__notch ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '">
 					<label
 						for="' . esc_attr( $id ) . '"
 						class=" ' . $classes . '"
-						style="' . $this->label_styles . '"
+						style="' . $this->label_styles . $output_data['css_vars'] . '"
 					>'
 			. esc_html( $label )
 			. ( $required ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
 			'</label>
 				</div>
-				<div class="notched-label__trailing"></div>
+				<div class="notched-label__filler ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
+				<div class="notched-label__trailing ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
 			</div>';
 	}
 
+	/**
+	 * Returns the styles, classes and CSS vars necessary to render fields in the "Outlined" style.
+	 * At the block level, the styles are extracted and added to the shortcode attributes in
+	 * Contact_Form_Plugin::get_outlined_style_attributes().
+	 * This function extracts those styles and applies them to the field,
+	 * and ensures any global or theme styles are applied.
+	 *
+	 * @param string $form_style (optional) The form style.
+	 *
+	 * @return array {
+	 *     @type string $style_attrs The style attributes.
+	 *     @type string $css_vars The CSS variables.
+	 *     @type string $class_name The class name.
+	 * }
+	 */
+	private function get_outline_styles( $form_style = 'outlined' ) {
+		$style_attrs     = '';
+		$css_vars        = '';
+		$outline_styles  = $this->get_attribute( 'outlinestyledata' );
+		$outline_classes = $this->get_attribute( 'outlinestyleclasses' );
+
+		if ( ! empty( $outline_styles ) ) {
+			$outline_styles = json_decode( html_entity_decode( $outline_styles, ENT_COMPAT ), true );
+			$input_styles   = $this->get_attribute( 'inputstyles' );
+
+			if ( ! empty( $input_styles ) ) {
+				$style_attrs = $input_styles;
+			}
+
+			$global_styles = wp_get_global_styles(
+				array( 'border' ),
+				array(
+					'block_name' => 'jetpack/input',
+					'transforms' => array( 'resolve-variables' ),
+				)
+			);
+
+			$legacy_border_size   = $outline_styles['border']['width'] ?? null;
+			$legacy_border_size   = is_numeric( $legacy_border_size ) ? $legacy_border_size . 'px' : $legacy_border_size;
+			$legacy_border_radius = $outline_styles['border']['radius'] ?? null;
+			$legacy_border_radius = is_numeric( $legacy_border_radius ) ? $legacy_border_radius . 'px' : $legacy_border_radius;
+
+			$border_size = $legacy_border_size ??
+				$outline_styles['border']['top']['width'] ??
+				$global_styles['width'] ??
+				$global_styles['top']['width'];
+
+			$border_radius = $legacy_border_radius ??
+				$global_styles['radius'];
+
+			$css_vars = $border_size ? '--jetpack--contact-form--border-size: ' . $border_size . ';' : '';
+			// Check if border radius is split or a single value.
+			if ( is_array( $border_radius ) ) {
+				// If corner radii are set on the top-left or bottom-left of the block, take the maximum of the two.
+				// We check the left side due to writing direction—this variable is used to offset text.
+				// TODO: this should factor in RTL languages.
+				$css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: max(' . $border_radius['topLeft'] . ',' . $border_radius['bottomLeft'] . ');' : '';
+			} elseif ( isset( $border_radius ) ) {
+				$css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: ' . $border_radius . ';' : '';
+			}
+
+			if ( 'outlined' === $form_style ) {
+				$css_vars .= '--jetpack--contact-form--notch-width: max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius));';
+			} elseif ( 'animated' === $form_style ) {
+				$legacy_border_left_size = $outline_styles['border']['width'] ?? null;
+				$legacy_border_left_size = is_numeric( $legacy_border_left_size ) ? $legacy_border_left_size . 'px' : $legacy_border_left_size;
+
+				$border_left_size = $legacy_border_left_size ??
+				$outline_styles['border']['left']['width'] ??
+				$global_styles['width'] ??
+				$global_styles['left']['width'];
+
+				$css_vars .= "--jetpack--contact-form--left-offset: calc(var(--jetpack--contact-form--input-padding-left, 16px) + {$border_left_size});";
+				$css_vars .= '--jetpack--contact-form--label-left: max(var(--jetpack--contact-form--left-offset), var(--jetpack--contact-form--border-radius));';
+				$css_vars .= "--jetpack--contact-form--field-padding: calc(var(--jetpack--contact-form--label-left) - {$border_left_size});";
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: ramonjd
body: This PR simplifies these CSS vars https://github.com/Automattic/jetpack/pull/43469

I'm not convinced they're required for the animated style. 
created_at: 2025-05-15T05:54:40Z
author_association: MEMBER
diff_hunk: @@ -1142,13 +1164,22 @@ public function render_field( $attributes ) {
 	 * Gets the first div in the input html.
 	 *
 	 * @param string $html The html string.
+	 * @param array  $contact_form_attributes An associative array containing the contact form's attributes.
 	 *
 	 * @return DOMElement The first div element.
 	 */
-	public function getCommonDiv( $html ) {
+	public function getCommonDiv( $html, $contact_form_attributes = array() ) {
 		$doc = new DOMDocument();
 		$doc->loadHTML( $html );
-		return $this->getFirstElement( $doc, 'div' );
+		$first_el = $this->getFirstElement( $doc, 'div' );
+		/**
+		 * If the contact form has the `is-style-outlined` class name, we need to get the second div element.
+		 * This is because, to achieve the outlined effect, the first div is the wrapper div, and the second div is the field wrapper div.
+		 */
+		if ( isset( $contact_form_attributes['className'] ) && 'is-style-outlined' === $contact_form_attributes['className'] ) {
+			$first_el = $this->getFirstElement( $doc, 'div', 1 );
+		}
+		return $first_el;
path: projects/packages/forms/tests/php/contact-form/Contact_Form_Test.php
user: login: ramonjd
body: You know your code smells when you have to document the unit tests 😄 
created_at: 2025-05-15T05:55:24Z
author_association: MEMBER
diff_hunk: @@ -1151,13 +1221,59 @@ private function get_unauth_endpoint_url() {
 	 * @return string HTML
 	 */
 	public function render_checkbox_multiple_field( $id, $label, $value, $class, $required, $required_field_text ) {
+		$options_classes   = $this->get_attribute( 'optionsclasses' );
+		$options_styles    = $this->get_attribute( 'optionsstyles' );
+		$form_style        = $this->get_form_style();
+		$is_outlined_style = 'outlined' === $form_style;
+
 		// The `data-required` attribute is used in `accessible-form.js` to ensure at least one
 		// checkbox is checked. Unlike radio buttons, for which the required attribute is satisfied if
 		// any of the radio buttons in the group is selected, adding a required attribute directly to
 		// a checkbox means that this specific checkbox must be checked.
-		$field  = '<fieldset id="' . esc_attr( "$id-label" ) . '" class="grunion-checkbox-multiple-options"' . ( $required ? ' data-required' : '' ) . '>';
+
+		$fieldset_id = "id='" . esc_attr( "$id-label" ) . "'";
+
+		$outline_border_radius = '';
+		if ( $is_outlined_style ) {
+			$outline_styles = $this->get_attribute( 'outlinestyledata' );
+
+			if ( ! empty( $outline_styles ) ) {
+				$outline_styles = json_decode( html_entity_decode( $outline_styles, ENT_COMPAT ), true );
+			}
+
+			// When there's an outlined style, and border radius is set, we override the inline border radius to apply
+			// a limit of `100px` to the radius on the x axis. This achieves the same look and feel as other fields
+			// that use the notch html (`notched-label__leading` has a max-width of `100px` to prevent it from getting too wide).
+			if ( isset( $outline_styles['border']['radius'] ) ) {
+				$radius                  = $outline_styles['border']['radius'];
+				$has_split_radius_values = is_array( $radius );
+				$top_left_radius         = $has_split_radius_values ? $radius['topLeft'] : $radius;
+				$top_right_radius        = $has_split_radius_values ? $radius['topRight'] : $radius;
+				$bottom_left_radius      = $has_split_radius_values ? $radius['bottomLeft'] : $radius;
+				$bottom_right_radius     = $has_split_radius_values ? $radius['bottomRight'] : $radius;
+				$outline_border_radius  .= "border-top-left-radius: min(100px, {$top_left_radius}) {$top_left_radius};";
+				$outline_border_radius  .= "border-top-right-radius: min(100px, {$top_right_radius}) {$top_right_radius};";
+				$outline_border_radius  .= "border-bottom-left-radius: min(100px, {$bottom_left_radius}) {$bottom_left_radius};";
+				$outline_border_radius  .= "border-bottom-right-radius: min(100px, {$bottom_right_radius}) {$bottom_right_radius};";
+
+			}
+		}
+
+		/*
+		 * For the "outlined" style, the styles and classes are applied to the fieldset element.
+		 */
+		if ( $is_outlined_style ) {
+			$field = "<fieldset {$fieldset_id} class='wp-block-jetpack-options grunion-checkbox-multiple-options" . $options_classes . "' style='" . $options_styles . $outline_border_radius . "' " . ( $required ? 'data-required' : '' ) . '>';
+		} else {
+			$field = "<fieldset {$fieldset_id} class='jetpack-field-multiple__fieldset'>";
+		}
+
 		$field .= $this->render_legend_as_label( '', $id, $label, $required, $required_field_text );
 
+		if ( ! $is_outlined_style ) {
+			$field .= "<div class='wp-block-jetpack-options grunion-checkbox-multiple-options" . $options_classes . "' style='" . $options_styles . "' " . ( $required ? 'data-required' : '' ) . '>';
+		}
+
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: talldan
body: I guess we can combine those consecutive `if ( $is_outlined_style ) {` statements as well.
created_at: 2025-05-15T06:39:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -39,7 +39,6 @@ export default function DateFieldEdit( props ) {
 		template,
 		templateLock: 'all',
 	} );
-
path: projects/packages/forms/src/blocks/field-date/edit.js
user: login: talldan
body: I'm not so sure. I think everything depends on this change. I don't want to see the whole house of cards come crashing down.
created_at: 2025-05-15T06:40:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -39,7 +39,6 @@ export default function DateFieldEdit( props ) {
 		template,
 		templateLock: 'all',
 	} );
-
path: projects/packages/forms/src/blocks/field-date/edit.js
user: login: ramonjd
body: You're right.

This is hilarious and tragic at the same time. 🤣 

Hilaragic


created_at: 2025-05-15T06:43:58Z
author_association: MEMBER
diff_hunk: @@ -145,20 +149,35 @@ public static function register_child_blocks() {
 				'uses_context' => array(
 					'jetpack/field-required',
 					'jetpack/field-dateFormat',
+					'jetpack/field-share-input-type',
path: projects/packages/forms/src/blocks/contact-form/class-contact-form-block.php
user: login: talldan
body: @ramonjd Is this something we can remove? I don't see it used anywhere.
created_at: 2025-05-15T06:57:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,177 @@
+import {
+	__experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetColorClassesAndStyles as getColorClassesAndStyles, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import { isNumber, merge } from 'lodash';
+import { useFormStyle, FORM_STYLE } from '../../contact-form/util/form';
+
+/**
+ * Returns the value of the CSS var if it is a number, otherwise null.
+ * Match behaviour in projects/packages/forms/src/blocks/shared/hooks/use-jetpack-field-styles.js.
+ *
+ * @param {*} value - A value from the legacy form block attributes.
+ * @return {string|*} The value of the CSS var if it is a number, otherwise the value itself.
+ */
+function getIntAsPxValue( value ) {
+	if ( typeof value !== 'undefined' && isNumber( value ) ) {
+		return `${ value }px`;
+	}
+	return value;
+}
+
+/**
+ * Returns the value of the CSS var for the border radius for outlined forms, taking into account
+ * that the border radius might be a single or split value.
+ *
+ * @param {object} style - The style object.
+ *
+ * @return {string|undefined} The CSS var for the border radius.
+ */
+function getBorderRadiusCssVar( style ) {
+	// A single border radius value for all for corners, this is quicker to check, so it goes first.
+	if ( style?.borderRadius ) {
+		return getIntAsPxValue( style?.borderRadius );
+	}
+	// If corner radii are set on the top-left or bottom-left of the block, take the maximum of the two.
+	// We check the left side due to writing direction—this variable is used to offset text.
+	// TODO: this should factor in RTL languages.
+	if ( style?.borderTopLeftRadius || style?.borderBottomLeftRadius ) {
+		return `max( ${ style?.borderTopLeftRadius ?? 0 }, ${ style?.borderBottomLeftRadius ?? 0 } )`;
path: projects/packages/forms/src/blocks/shared/hooks/use-variation-style-properties.js
user: login: talldan
body: I think it might be better to refactor to individual css vars like this:
```js
--jetpack--contact-form--border-top-left-radius: ${ style?.borderRadius ?? style?.borderTopLeftRadius };
--jetpack--contact-form--border-top-right-radius: ${ style?.borderRadius ?? style?.borderTopRightRadius };
--jetpack--contact-form--border-bottom-right-radius: ${ style?.borderRadius ?? style?.borderBottomRightRadius };
--jetpack--contact-form--border-bottom-left-radius: ${ style?.borderRadius ?? style?.borderBottomLeftRadius };
```

Assuming a single border radius value from the two corners is a shortcut to get it working and not very clear in terms of the naming of the css var. It's something that could cause confusion for devs later, so I think better to be explicit with the naming.

Possibly the same for the border size css var.

However, I might leave it to later for a smaller PR.
created_at: 2025-05-15T08:33:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1391,23 +1506,111 @@ public function render_outline_label( $id, $label, $required, $required_field_te
 		$classes .= $this->is_error() ? ' form-error' : '';
 		$classes .= $this->label_classes ? ' ' . $this->label_classes : '';
 
+		$output_data = $this->get_outline_styles();
+
 		return '
 			<div class="notched-label">
-				<div class="notched-label__leading"></div>
-				<div class="notched-label__notch">
+				<div class="notched-label__leading ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
+				<div class="notched-label__notch ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '">
 					<label
 						for="' . esc_attr( $id ) . '"
 						class=" ' . $classes . '"
-						style="' . $this->label_styles . '"
+						style="' . $this->label_styles . $output_data['css_vars'] . '"
 					>'
 			. esc_html( $label )
 			. ( $required ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
 			'</label>
 				</div>
-				<div class="notched-label__trailing"></div>
+				<div class="notched-label__filler ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
+				<div class="notched-label__trailing ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
 			</div>';
 	}
 
+	/**
+	 * Returns the styles, classes and CSS vars necessary to render fields in the "Outlined" style.
+	 * At the block level, the styles are extracted and added to the shortcode attributes in
+	 * Contact_Form_Plugin::get_outlined_style_attributes().
+	 * This function extracts those styles and applies them to the field,
+	 * and ensures any global or theme styles are applied.
+	 *
+	 * @param string $form_style (optional) The form style.
+	 *
+	 * @return array {
+	 *     @type string $style_attrs The style attributes.
+	 *     @type string $css_vars The CSS variables.
+	 *     @type string $class_name The class name.
+	 * }
+	 */
+	private function get_outline_styles( $form_style = 'outlined' ) {
+		$style_attrs     = '';
+		$css_vars        = '';
+		$outline_styles  = $this->get_attribute( 'outlinestyledata' );
+		$outline_classes = $this->get_attribute( 'outlinestyleclasses' );
+
+		if ( ! empty( $outline_styles ) ) {
+			$outline_styles = json_decode( html_entity_decode( $outline_styles, ENT_COMPAT ), true );
+			$input_styles   = $this->get_attribute( 'inputstyles' );
+
+			if ( ! empty( $input_styles ) ) {
+				$style_attrs = $input_styles;
+			}
+
+			$global_styles = wp_get_global_styles(
+				array( 'border' ),
+				array(
+					'block_name' => 'jetpack/input',
+					'transforms' => array( 'resolve-variables' ),
+				)
+			);
+
+			$legacy_border_size   = $outline_styles['border']['width'] ?? null;
+			$legacy_border_size   = is_numeric( $legacy_border_size ) ? $legacy_border_size . 'px' : $legacy_border_size;
+			$legacy_border_radius = $outline_styles['border']['radius'] ?? null;
+			$legacy_border_radius = is_numeric( $legacy_border_radius ) ? $legacy_border_radius . 'px' : $legacy_border_radius;
+
+			$border_size = $legacy_border_size ??
+				$outline_styles['border']['top']['width'] ??
+				$global_styles['width'] ??
+				$global_styles['top']['width'];
+
+			$border_radius = $legacy_border_radius ??
+				$global_styles['radius'];
+
+			$css_vars = $border_size ? '--jetpack--contact-form--border-size: ' . $border_size . ';' : '';
+			// Check if border radius is split or a single value.
+			if ( is_array( $border_radius ) ) {
+				// If corner radii are set on the top-left or bottom-left of the block, take the maximum of the two.
+				// We check the left side due to writing direction—this variable is used to offset text.
+				// TODO: this should factor in RTL languages.
+				$css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: max(' . $border_radius['topLeft'] . ',' . $border_radius['bottomLeft'] . ');' : '';
+			} elseif ( isset( $border_radius ) ) {
+				$css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: ' . $border_radius . ';' : '';
+			}
+
+			if ( 'outlined' === $form_style ) {
+				$css_vars .= '--jetpack--contact-form--notch-width: max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius));';
+			} elseif ( 'animated' === $form_style ) {
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: talldan
body: We should rename the function to something other than `get_outline_styles`, as it also handles animated styles.
created_at: 2025-05-15T08:37:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1391,23 +1509,111 @@ public function render_outline_label( $id, $label, $required, $required_field_te
 		$classes .= $this->is_error() ? ' form-error' : '';
 		$classes .= $this->label_classes ? ' ' . $this->label_classes : '';
 
+		$output_data = $this->get_outline_styles();
+
 		return '
 			<div class="notched-label">
-				<div class="notched-label__leading"></div>
-				<div class="notched-label__notch">
+				<div class="notched-label__leading ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
+				<div class="notched-label__notch ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '">
 					<label
 						for="' . esc_attr( $id ) . '"
 						class=" ' . $classes . '"
-						style="' . $this->label_styles . '"
+						style="' . $this->label_styles . $output_data['css_vars'] . '"
 					>'
 			. esc_html( $label )
 			. ( $required ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
 			'</label>
 				</div>
-				<div class="notched-label__trailing"></div>
+				<div class="notched-label__filler ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
+				<div class="notched-label__trailing ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
 			</div>';
 	}
 
+	/**
+	 * Returns the styles, classes and CSS vars necessary to render fields in the "Outlined" style.
+	 * At the block level, the styles are extracted and added to the shortcode attributes in
+	 * Contact_Form_Plugin::get_outlined_style_attributes().
+	 * This function extracts those styles and applies them to the field,
+	 * and ensures any global or theme styles are applied.
+	 *
+	 * @param string $form_style (optional) The form style.
+	 *
+	 * @return array {
+	 *     @type string $style_attrs The style attributes.
+	 *     @type string $css_vars The CSS variables.
+	 *     @type string $class_name The class name.
+	 * }
+	 */
+	private function get_outline_styles( $form_style = 'outlined' ) {
+		$style_attrs     = '';
+		$css_vars        = '';
+		$outline_styles  = $this->get_attribute( 'outlinestyledata' );
+		$outline_classes = $this->get_attribute( 'outlinestyleclasses' );
+
+		if ( ! empty( $outline_styles ) ) {
+			$outline_styles = json_decode( html_entity_decode( $outline_styles, ENT_COMPAT ), true );
+			$input_styles   = $this->get_attribute( 'inputstyles' );
+
+			if ( ! empty( $input_styles ) ) {
+				$style_attrs = $input_styles;
+			}
+
+			$global_styles = wp_get_global_styles(
+				array( 'border' ),
+				array(
+					'block_name' => 'jetpack/input',
+					'transforms' => array( 'resolve-variables' ),
+				)
+			);
+
+			$legacy_border_size   = $outline_styles['border']['width'] ?? null;
+			$legacy_border_size   = is_numeric( $legacy_border_size ) ? $legacy_border_size . 'px' : $legacy_border_size;
+			$legacy_border_radius = $outline_styles['border']['radius'] ?? null;
+			$legacy_border_radius = is_numeric( $legacy_border_radius ) ? $legacy_border_radius . 'px' : $legacy_border_radius;
+
+			$border_size = $legacy_border_size ??
+				$outline_styles['border']['top']['width'] ??
+				$global_styles['width'] ??
+				$global_styles['top']['width'];
+
+			$border_radius = $legacy_border_radius ??
+				$global_styles['radius'];
+
+			$css_vars = $border_size ? '--jetpack--contact-form--border-size: ' . $border_size . ';' : '';
+			// Check if border radius is split or a single value.
+			if ( is_array( $border_radius ) ) {
+				// If corner radii are set on the top-left or bottom-left of the block, take the maximum of the two.
+				// We check the left side due to writing direction—this variable is used to offset text.
+				// TODO: this should factor in RTL languages.
+				$css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: max(' . $border_radius['topLeft'] . ',' . $border_radius['bottomLeft'] . ');' : '';
+			} elseif ( isset( $border_radius ) ) {
+				$css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: ' . $border_radius . ';' : '';
+			}
+
+			if ( 'outlined' === $form_style ) {
+				$css_vars .= '--jetpack--contact-form--notch-width: max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius));';
+			} elseif ( 'animated' === $form_style ) {
+				$legacy_border_left_size = $outline_styles['border']['width'] ?? null;
+				$legacy_border_left_size = is_numeric( $legacy_border_left_size ) ? $legacy_border_left_size . 'px' : $legacy_border_left_size;
+
+				$border_left_size = $legacy_border_left_size ??
+				$outline_styles['border']['left']['width'] ??
+				$global_styles['width'] ??
+				$global_styles['left']['width'];
+
+				$css_vars .= "--jetpack--contact-form--left-offset: calc(var(--jetpack--contact-form--input-padding-left, 16px) + {$border_left_size});";
+				$css_vars .= '--jetpack--contact-form--label-left: max(var(--jetpack--contact-form--left-offset), var(--jetpack--contact-form--border-radius));';
+				$css_vars .= "--jetpack--contact-form--field-padding: calc(var(--jetpack--contact-form--label-left) - {$border_left_size});";
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: ramonjd
body: @talldan 

Good news I think. I think we can remove the variable left padding for the Outlined style when there's lots of radius and border.

We've replicated the behaviour from trunk, but in trunk the indentation only occurs in the editor, and has no effect on the frontend in my testing.

Here's trunk:

| Trunk editor | Trunk frontend |
|--------|--------|
| <img width="400" alt="Screenshot 2025-05-16 at 11 07 08 am" src="https://github.com/user-attachments/assets/7b89d2c6-f68e-479d-a4b0-4c6744c2911f" /> | <img width="400" alt="Screenshot 2025-05-16 at 11 07 25 am" src="https://github.com/user-attachments/assets/8df3e23f-561d-4d39-8c3e-477471928457" /> |





created_at: 2025-05-16T01:10:12Z
author_association: MEMBER
diff_hunk: @@ -145,20 +149,35 @@ public static function register_child_blocks() {
 				'uses_context' => array(
 					'jetpack/field-required',
 					'jetpack/field-dateFormat',
+					'jetpack/field-share-input-type',
path: projects/packages/forms/src/blocks/contact-form/class-contact-form-block.php
user: login: ramonjd
body: Oh yeah, soz. That is probably the hangover of some left-field idea.
created_at: 2025-05-16T03:47:06Z
author_association: MEMBER
diff_hunk: @@ -1391,23 +1506,111 @@ public function render_outline_label( $id, $label, $required, $required_field_te
 		$classes .= $this->is_error() ? ' form-error' : '';
 		$classes .= $this->label_classes ? ' ' . $this->label_classes : '';
 
+		$output_data = $this->get_outline_styles();
+
 		return '
 			<div class="notched-label">
-				<div class="notched-label__leading"></div>
-				<div class="notched-label__notch">
+				<div class="notched-label__leading ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
+				<div class="notched-label__notch ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '">
 					<label
 						for="' . esc_attr( $id ) . '"
 						class=" ' . $classes . '"
-						style="' . $this->label_styles . '"
+						style="' . $this->label_styles . $output_data['css_vars'] . '"
 					>'
 			. esc_html( $label )
 			. ( $required ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
 			'</label>
 				</div>
-				<div class="notched-label__trailing"></div>
+				<div class="notched-label__filler ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
+				<div class="notched-label__trailing ' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
 			</div>';
 	}
 
+	/**
+	 * Returns the styles, classes and CSS vars necessary to render fields in the "Outlined" style.
+	 * At the block level, the styles are extracted and added to the shortcode attributes in
+	 * Contact_Form_Plugin::get_outlined_style_attributes().
+	 * This function extracts those styles and applies them to the field,
+	 * and ensures any global or theme styles are applied.
+	 *
+	 * @param string $form_style (optional) The form style.
+	 *
+	 * @return array {
+	 *     @type string $style_attrs The style attributes.
+	 *     @type string $css_vars The CSS variables.
+	 *     @type string $class_name The class name.
+	 * }
+	 */
+	private function get_outline_styles( $form_style = 'outlined' ) {
+		$style_attrs     = '';
+		$css_vars        = '';
+		$outline_styles  = $this->get_attribute( 'outlinestyledata' );
+		$outline_classes = $this->get_attribute( 'outlinestyleclasses' );
+
+		if ( ! empty( $outline_styles ) ) {
+			$outline_styles = json_decode( html_entity_decode( $outline_styles, ENT_COMPAT ), true );
+			$input_styles   = $this->get_attribute( 'inputstyles' );
+
+			if ( ! empty( $input_styles ) ) {
+				$style_attrs = $input_styles;
+			}
+
+			$global_styles = wp_get_global_styles(
+				array( 'border' ),
+				array(
+					'block_name' => 'jetpack/input',
+					'transforms' => array( 'resolve-variables' ),
+				)
+			);
+
+			$legacy_border_size   = $outline_styles['border']['width'] ?? null;
+			$legacy_border_size   = is_numeric( $legacy_border_size ) ? $legacy_border_size . 'px' : $legacy_border_size;
+			$legacy_border_radius = $outline_styles['border']['radius'] ?? null;
+			$legacy_border_radius = is_numeric( $legacy_border_radius ) ? $legacy_border_radius . 'px' : $legacy_border_radius;
+
+			$border_size = $legacy_border_size ??
+				$outline_styles['border']['top']['width'] ??
+				$global_styles['width'] ??
+				$global_styles['top']['width'];
+
+			$border_radius = $legacy_border_radius ??
+				$global_styles['radius'];
+
+			$css_vars = $border_size ? '--jetpack--contact-form--border-size: ' . $border_size . ';' : '';
+			// Check if border radius is split or a single value.
+			if ( is_array( $border_radius ) ) {
+				// If corner radii are set on the top-left or bottom-left of the block, take the maximum of the two.
+				// We check the left side due to writing direction—this variable is used to offset text.
+				// TODO: this should factor in RTL languages.
+				$css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: max(' . $border_radius['topLeft'] . ',' . $border_radius['bottomLeft'] . ');' : '';
+			} elseif ( isset( $border_radius ) ) {
+				$css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: ' . $border_radius . ';' : '';
+			}
+
+			if ( 'outlined' === $form_style ) {
+				$css_vars .= '--jetpack--contact-form--notch-width: max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius));';
+			} elseif ( 'animated' === $form_style ) {
path: projects/packages/forms/src/contact-form/class-contact-form-field.php
user: login: ramonjd
body: I'll do this in https://github.com/Automattic/jetpack/pull/43469
created_at: 2025-05-16T05:18:51Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-01T07:56:31Z
body: <!-- wpcom-reminder-comment -->
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

- To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate [the Jetpack Beta plugin](https://jetpack.com/download-jetpack-beta/). Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the `try/migrate-form-outline-style` branch.
- To test on Simple, run the following command on your sandbox:
```
bin/jetpack-downloader test jetpack try/migrate-form-outline-style
```

*Interested in more tips and information?*

- In your local development environment, use the `jetpack rsync` command to sync your changes to a WoA dev blog.
- Read more about our development workflow here: PCYsg-eg0-p2
- Figure out **when your changes will be shipped to customers** here: PCYsg-eg5-p2
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-01T07:56:50Z
body: **Thank you for your PR!**

When contributing to Jetpack, we have [a few suggestions](https://github.com/Automattic/jetpack/blob/trunk/.github/PULL_REQUEST_TEMPLATE.md) that can help us test and review your patch:<br>
- :white_check_mark: Include a description of your PR changes.<br>
- :red_circle: Add a "[Status]" label (In Progress, Needs Review, ...).<br>
- :red_circle: Add a "[Type]" label (Bug, Enhancement, Janitorial, Task).<br>
- :white_check_mark: Add testing instructions.<br>
- :red_circle: Specify whether this PR includes any changes to data or privacy.<br>
- :red_circle: Add changelog entries to affected projects<br>


This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation :robot:

******
:red_circle: **Action required:** We would recommend that you add a section to the PR description to specify whether this PR includes any changes to data or privacy, like so:
~~~
## Does this pull request change what data or activity we track or use?

My PR adds *x* and *y*.
~~~

******
:red_circle: **Action required:** Please add missing changelog entries for the following projects: `projects/packages/forms`

Use [the Jetpack CLI tool](https://github.com/Automattic/jetpack/blob/trunk/docs/monorepo.md#first-time) to generate changelog entries by running the following command: `jetpack changelog add`.
Guidelines: [/docs/writing-a-good-changelog-entry.md](https://github.com/Automattic/jetpack/blob/trunk/docs/writing-a-good-changelog-entry.md)


******

**Follow this PR Review Process:**

1. Ensure all required checks appearing at the bottom of this PR are passing.
2. Make sure to test your changes on all platforms that it applies to. *You're responsible for the quality of the code you ship*.
3. You can use [GitHub's Reviewers functionality](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) to request a review.
4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!
******

**Jetpack plugin:**

The Jetpack plugin has different release cadences depending on the platform:

- WordPress.com Simple releases happen as soon as you deploy your changes after merging this PR (PCYsg-Jjm-p2).
- WoA releases happen weekly.
- Releases to self-hosted sites happen monthly:
    - Scheduled release: _June 3, 2025_
    - Code freeze: _June 2, 2025_


If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

author_association: CONTRIBUTOR
user: login: jp-launch-control[bot]
created_at: 2025-05-01T08:12:02Z
body: ### Code Coverage Summary

No summary data is available for parent commit e9c1ade8, so cannot calculate coverage changes. :sleeping:

If that commit is a feature branch rather than a trunk commit, this is expected. Otherwise, this should be updated once coverage for e9c1ade8 is available.

[Full summary](https://jetpackcodecoverage.atomicsites.blog/prs/43327/) · [PHP report](https://jetpackcodecoverage.atomicsites.blog/prs/43327/php/) · [JS report](https://jetpackcodecoverage.atomicsites.blog/prs/43327/js/)
author_association: NONE
user: login: talldan
created_at: 2025-05-02T05:01:51Z
body: Thanks for exploring this. It looks like this solution has required a lot of jumping through hoops to get to something that works 😭 

It feels very complicated, especially on the editor side, I have some concerns over the maintenance burden this would leave in the future.

Thought I'd also mention that I see a similar issue with the animated form style, labels aren't offset correctly when applying a thicker border and will overlap the border. Perhaps the border needs to be applied to the field and not the input, so it's a similar problem of this style requiring that border isn't applied to inputs:
<img width="387" alt="Screenshot 2025-05-02 at 11 53 41 am" src="https://github.com/user-attachments/assets/74c31e9b-b398-41bb-b788-08ef6a4b4593" />

I think perhaps we shouldn't be afraid of thinking completely outside the box on this. I'm mentioning ever idea I have, whether good or bad:
- Deprecating the outlined/animated styles could be an option. I personally wish someone had considered how complicated they were originally and decided to pull the plug!
- Could adding border supports to field blocks make solving these bugs easier?
- Alternatively, add border support to the label block since this is the block that has the border. Perhaps it's not intuitive for a user, but I also think this is a very niché feature.
- Could we disable border controls when outlined/animated style is active? I can't think of a way, but I thought I'd mention the idea. If it were possible, I think it might be my preferred option.
- It feels like we're being quite respectful to the html used to achieve the 'notched' look. Are there alternatives, like adding the extra html around the input element rather than the label? The input block would need to know the width of the label text (or what the label text is) to achieve the cut-out border, so it's a similar problem of a block being aware of the props of its sibling, but maybe slightly simpler given how complex it is in this PR to resolve the applied style in the editor. 🤔  (edit: I realize we also need all the font styles (size, letter spacing) to make the cut-out accurate, so this is probably also a non-starter).
- I've also been wondering if there are any css tricks to achieve a similar look (e.g using `border-image` for the cut-out), but I haven't found anything that doesn't come with other issues (`border-image` doesn't support `border-radius`).
author_association: CONTRIBUTOR
user: login: andrewserong
created_at: 2025-05-02T06:05:51Z
body: > Thanks for exploring this.

💯 this is a very thorny one, thanks for diving deep on it!

> Could we disable border controls when outlined/animated style is active? I can't think of a way, but I thought I'd mention the idea. If it were possible, I think it might be my preferred option.

This is a good point if we think this winds up being too complex to fix for these styles — I've been trying to think of how it could be possible to dynamically disable the controls. Unfortunately the simple filter for removing support altogether is all or nothing rather than based on conditions, so the following wouldn't work for us:

```js
const skipBorders = settings => {
	if ( settings.name.indexOf( 'jetpack/input' ) > -1 ) {
		if ( settings.supports.__experimentalBorder ) {
			delete settings.supports.__experimentalBorder;
		}
	}
	return settings;
};

addFilter( 'blocks.registerBlockType', 'jetpack/contact-form/skip-borders', skipBorders );
```

Something else I'm wondering — it wouldn't be performant, but would it be possible to add a hook to the `InputEdit` component where we update the block editor settings if we're in a field block that's a child of a block using the Outlined styles? So we'd update `settings.__experimentalFeatures.blocks.jetpack/field/border` to be set to false (or its border props set to false). Then as a cleanup task, update again to remove that / reset to the original settings? I suppose I'm imagining something similar(ish) to `useSyncedAttributes` in that sense 🤔. I haven't tried this, so this is just an idle thought!
author_association: MEMBER
user: login: talldan
created_at: 2025-05-02T07:21:27Z
body: While experimenting/researching I found a way to achieve the cut-out in the border using css `clip-path`: https://codepen.io/talldan/pen/JoopZNO

This might allow us to keep the border applied to the input, but two issues:
- it's a minor thing, but it also clips the input's background if we wanted it to extend under the border.
- We'd need to know exactly where the clipping should start and end, which I think means getting the computed size and position of the label. That's not something I love the idea of, especially when you factor in every input on the screen needing to do this. 😬 
author_association: CONTRIBUTOR
user: login: ramonjd
created_at: 2025-05-04T23:39:44Z
body: Thanks @talldan and @andrewserong for helping to think about this issue. 

I took a pretty naive approach first off, but my main objectives were to cause minimum disruption to the users running this style.

Now, this is despite the fact that this style — at least in terms of borders — does not work in the first place: on trunk, alternative border styles do not have any effect on radio and checkbox fields in my testing

https://github.com/user-attachments/assets/9560d3a8-ffaa-48cc-928d-53222fe6b836

So feature parity rather than feature fixing 😄 

My impressions while working on this were similar to yours: I'm having serious doubts about the feasibility of this exercise, and I'd expect, with good intentions, we'd be doing users a favor by deprecating outline and possible animated styles.

> It feels very complicated, especially on the editor side, I have some concerns over the maintenance burden this would leave in the future.

This is true. 💯 

What's also true is that this style is already seriously complicated, and brutally fragile, by which I mean the maintenance burden already exists.

I think we'd need to weigh inconvenience to developers against user experience - which should win here?

Clearly users of this style should not be in a place where their forms break, but we also shouldn't allow users to dive deeper into a hole (the hole being the outline style) and run the risk of committing to a broken and buggy style.

> Deprecating the outlined/animated styles could be an option. I personally wish someone had considered how complicated they were originally and decided to pull the plug!

+1

I would prefer deprecation, I'm just not sure how we should tackle the UX flow.

We need to make sure forms on the frontend won't be unaffected until the user edits a post and the form "migrates". That means keeping the current code, or a different version of it around.

It will cause confusion if the form in the editor looks radically different to what the user expects.

Furthermore, the user might have several outline forms on their site, all of which would have to be migrated separately assuming they need to look the same.

I think, in the case of deprecation, we'd have to inform the user of the situation in the editor, that is that editing the page would reset/remove the styles.

> Could we disable border controls when outlined/animated style is active? I can't think of a way, but I thought I'd mention the idea. If it were possible, I think it might be my preferred option. 

Let's say there's a performant way, or @andrewserong's idea above works, we'd still be facing the same conundrum of what to do with existing forms that have border styles. 

What the UX here? A user would open their post with an outline form and any borders they've applied wouldn't be there?

I don't know. 

> While experimenting/researching I found a way to achieve the cut-out in the border using css clip-path:

Thanks for following up on this! Do you think it's worth investing time in going down that path?

Or would we be replacing the outline feature with another of equal complexity?




author_association: MEMBER
user: login: andrewserong
created_at: 2025-05-05T00:59:56Z
body: Great points @ramonjd!

> What's also true is that this style is already seriously complicated, and brutally fragile, by which I mean the maintenance burden already exists.
> I think we'd need to weigh inconvenience to developers against user experience - which should win here?

I think your instincts are right to prioritise the user experience. Of all the options we've discussed so far, the direction of this PR sounds the most promising to me.

Deprecating these styles sounds appealing since it'd involve reducing what we maintain, and hiding options for a style we're not sure of how to support also has its appeal. Yet I can't help but feel that in both of those cases we're actively creating a worse UX, and thinking back over my comment from last week, I don't really see how we could meaningfully "switch off" the border controls when the outlined or animated styles are applied 🤔

Whereas with the approach here, within the context of the inner blocks refactor, we're making a best effort to try to maintain compatibility. I think that's a worthy goal, but I do admit it's hard to weigh up!

Keen to hear what others think too, and I'll do some more testing of this PR, as well.
author_association: MEMBER
user: login: ramonjd
created_at: 2025-05-05T01:12:01Z
body: For what it's worth, here's the current state of this branch. It's pretty close.


### Building a form in trunk

https://github.com/user-attachments/assets/a1a84d70-5a72-49eb-8330-cd26c135d2aa



### Switching to this branch


https://github.com/user-attachments/assets/b84008d3-5e42-47d3-9bfa-734ce37653b4



There are one or two things left to do as you can see in the last video, namely, ensuring that global styles/theme.json presets are applied correctly. I'm working on that now. 

There is one material improvement: the editor no longer lies to the user by applying border styles to checkbox fields 😄 

I reckon we can clean/abstract a few things to reduce the maintenance shock.

author_association: MEMBER
user: login: talldan
created_at: 2025-05-05T02:35:41Z
body: > What's also true is that this style is already seriously complicated, and brutally fragile, by which I mean the maintenance burden already exists.

It's the exact reason in my mind why we should choose not to support it and deprecate it. The alternative is to build something complicated on top of something that we admit is already fragile. It compounds the problem. A bad experience for users is one that is fragile and breaks. I'm not saying this PR is broken, but if the code is admittedly complex and built on something fragile, then there's a good chance it'll break in the future. For me, this is particularly the case because of the way this PR is using low level APIs to merge the global style layers. This is an API that WordPress should provide, rather than Jetpack having a custom implementation.

I think we should at least discuss what deprecation would look like. I think it could be something like this:
- Remove the style variations
- `is-style-animated` and `is-style-outline` become custom classnames for users
- The CSS for those styles has high enough specificity to override any global/block styles (hopefully just border) that might break the appearance
- The frontend rendering code has a condition that checks for `is-style-outline` class and inserts the extra markup (and code comments explaining why it does this.
- Tests are added to make sure it continues to work.

Anyway, if we choose to continue with this PR, I think we need to also not forget that the animated style is also broken, and make sure the solution works for both styles.
author_association: CONTRIBUTOR
user: login: ramonjd
created_at: 2025-05-05T03:51:18Z
body: > It's the exact reason in my mind why we should choose not to support it and deprecate it.

I'm all for deprecation, and would be more than happy to discontinue this PR. Also thanks for outlining the rough steps.

Something has to be done about these styles - we're forced into a corner either way if the inner blocks migration is to go ahead.

At least in terms of the outline style, it's already half broken so deprecating will be putting it out of its misery.

>  A bad experience for users is one that is fragile and breaks.

💯 

I think from a user's perspective, deprecating is a breaking change in the editor. That's the main concern I have. 

I think being upfront about it via a modal message or some other comms trick could mitigate. 

I can get a deprecate branch started so we can see it in action.










author_association: MEMBER
user: login: andrewserong
created_at: 2025-05-05T04:30:28Z
body: > I think from a user's perspective, deprecating is a breaking change in the editor. That's the main concern I have.

Same. The other question I have is about scope: but this can go either way — with the inner blocks refactor, how do we minimise how much we're changing at once? Unfortunately both deprecation and ensuring compatibility add complexity to the code change and to what we need to test 😞

> Also thanks for outlining the rough steps [...] I can get a deprecate branch started so we can see it in action.

Thank you both!

In these situations where we're trying to find a balance or trade-off, having branches to play with should help us figure out which way to go. Or at the very least, it'll help give us options we can point to when we ask other folks to help us decide!
author_association: MEMBER
user: login: talldan
created_at: 2025-05-05T04:33:46Z
body: > I think from a user's perspective, deprecating is a breaking change in the editor. That's the main concern I have.

I think it's ok personally, the main thing is to keep the same appearance on the frontend. Probably the main challenge is retaining custom border sizes. Though it looks kinda broken in trunk with the label not centered correctly, so there are probably not many users doing this:
<img width="664" alt="Screenshot 2025-05-05 at 12 30 26 pm" src="https://github.com/user-attachments/assets/1c08fea2-524e-4419-8903-4251c582f5ca" />

If we want to go an extra step, we could consider adding some messaging in the UI if the user has one of these deprecated styles active.

A nice follow-up to the whole global styles effort would be to introduce a new range of form style variations that use global styles properties entirely rather than custom css. I think it should be possible to come up with some better and simpler styles.
author_association: CONTRIBUTOR
user: login: talldan
created_at: 2025-05-05T05:04:36Z
body: Something I just remembered. The style variations were previously broken in the editor, not sure how long for, but possibly quite a long time - https://github.com/Automattic/jetpack/pull/41457. Maybe if I hadn't fixed them this whole conversation would be easier 😄 

@simison, it'd be great to get your thoughts on this too.
author_association: CONTRIBUTOR
user: login: ramonjd
created_at: 2025-05-05T06:44:23Z
body: > I think we should at least discuss what deprecation would look like. I think it could be something like this:


Here we go:

- https://github.com/Automattic/jetpack/pull/43366

Optionally, we could add an embedded alert in each form with a legacy style (not this one), but just to give you an idea:

<img width="771" alt="Screenshot 2025-05-05 at 4 43 47 pm" src="https://github.com/user-attachments/assets/e4ffae5e-c409-426d-9464-dbba5415cfe4" />



author_association: MEMBER
user: login: ramonjd
created_at: 2025-05-05T06:46:21Z
body: > The style variations were previously broken in the editor, not sure how long for, but possibly quite a long time - https://github.com/Automattic/jetpack/pull/41457. Maybe if I hadn't fixed them this whole conversation would be easier

Given that they've been broken for what seems like forever, I feel better about deprecation. 
author_association: MEMBER
user: login: ramonjd
created_at: 2025-05-06T02:09:05Z
body: I've been playing around with refactoring options and I can get something close...

https://codepen.io/ramonjd/pen/ByyxExd

But even then there'll be differences.

For the outline style, the first thing to dump is the `notch` HTML, and also accepting that a label background color must be set. 
author_association: MEMBER
user: login: talldan
created_at: 2025-05-06T05:20:31Z
body: > But even then there'll be differences.

> For the outline style, the first thing to dump is the notch HTML, and also accepting that a label background color must be set.

I think the tricky part here is that if a user wants a transparent background on an input, it becomes hard to support with the way the label has a white background. Especially if there's a background behind the input, and doubly so if that background is a gradient or an input. That's one of the reasons I thought to try using `clip-path` in that earlier codepen, but that also has tradeoffs.

Still, it would be nice to get rid of the notch html!
author_association: CONTRIBUTOR
user: login: talldan
created_at: 2025-05-06T05:48:34Z
body: There are a couple of things to solve related to border radius that I'm seeing:
<img width="594" alt="Screenshot 2025-05-06 at 1 31 41 pm" src="https://github.com/user-attachments/assets/cc8f0037-de23-4f23-8108-2e0e5db6719a" />

Most of the fields have white backgrounds (I think this is part of the outlined style), but the dropdown is missing this for some reason. Also a bigger issue is the background bleeding out beyond the border radius.

I'm not sure what the right solution for this is. We could try applying the same border-radius to the input, but that won't work automatically. The `notched-label__leading` element is quite narrow - it has a base width of 16px plus the border width. The way border radius works is that it has a max value of the width of the element, and then after that increasing the value has no effect. So if `notched-label__leading` is only 20px wide, it'll only receive a border radius of up to 20px.

Another side effect of this is the way in the screenshot the left side of the input has a different visible border-radius to the right side. That's because `notched-label__trailing` is much wider and can receive the full border-radius, while `notched-label__leading` is narrow and has a limited border-radius.

I think the way to solve it might be to ensure `notched-label__leading` has a min-width of whatever the border-radius is so that it can receive the full border radius. Then applying a matching border radius to the input might work. It'll have a side-effect though, the label will shift right as a result of that change, so we'll need to increase the right (and left) padding in the input to keep the label and input text aligned.

Alternatively we could try applying the background to the label instead of the input, but then we still need to fix the unbalanced border radii.

Nasty stuff!
author_association: CONTRIBUTOR
user: login: talldan
created_at: 2025-05-06T07:24:38Z
body: > I think the way to solve it might be to ensure notched-label__leading has a min-width of whatever the border-radius is so that it can receive the full border radius. Then applying a matching border radius to the input might work. It'll have a side-effect though, the label will shift right as a result of that change, so we'll need to increase the right (and left) padding in the input to keep the label and input text aligned.

Just further to this. I checked `trunk`, and that's how it works there. There's a CSS rule here to determine the notch width, and it takes border radius into account.:
https://github.com/Automattic/jetpack/blob/2701fba6bfcf11dbc9e8c064de782c968fd8ee69/projects/packages/forms/src/blocks/contact-form/editor.scss#L743
author_association: CONTRIBUTOR
user: login: ramonjd
created_at: 2025-05-06T07:44:40Z
body: > Most of the fields have white backgrounds (I think this is part of the outlined style), but the dropdown is missing this for some reason. Also a bigger issue is the background bleeding out beyond the border radius.

@talldan [97cb381](https://github.com/Automattic/jetpack/pull/43327/commits/97cb381349ca5b08887e4f845aadf7e83c7c1129) should have fixed the clipping. 

[95d6b11](https://github.com/Automattic/jetpack/pull/43327/commits/95d6b11f62a4494923a4d66abbb2d27c00dea2f8) balances the border radii.

The code is pretty all over the shop, but there's room for abstraction/tightening up.

Some things I noticed that may or may not be trunk bugs:

- The Phone field apparent has a placeholder, which excludes it from the animated label effect. This is curious. Looks like a bug.
- The checkbox/radio fields don't get styled, but this is a trunk thing. We've "improved" it here to prevent the editor lying to the user. It would be good to fix this bug as part of this PR, but let's see.
author_association: MEMBER
user: login: ramonjd
created_at: 2025-05-06T07:57:16Z
body: Block classnames are being applied to the notch element - I need to fix that.

![image](https://github.com/user-attachments/assets/645f1ba0-3d94-421d-a075-14965358547e)

Caught by @talldan 
author_association: MEMBER
user: login: ramonjd
created_at: 2025-05-16T01:04:43Z
body: In this testing I created 6 forms in trunk, then loaded them unchanged in this branch to compare.

The screenshots are all from the frontend.

It's mostly good.

| Style | Trunk | This branch |
|--------|--------|--------|
| Default | <img width="400" alt="Trunk default 1" src="https://github.com/user-attachments/assets/b5d64c25-49ab-49c5-8017-61f154ae1ad5" /> | <img width="400" alt="Feature default 1" src="https://github.com/user-attachments/assets/d52891ba-c240-4f99-8f9e-a167681f6590" /> |
| Outlined | <img width="400" alt="Trunk outlined 3" src="https://github.com/user-attachments/assets/8ffe18e4-f72e-4330-988b-f7ae3397e92d" /> | <img width="400" alt="Feature outlined 3" src="https://github.com/user-attachments/assets/295c4433-4bfb-4885-8add-d047e142f512" /> |
| Outlined | <img width="400" alt="Trunk outlined 2" src="https://github.com/user-attachments/assets/14b40873-b1f8-4a50-b433-7c9e6d335df3" /> | <img width="400" alt="Feature outlined 2" src="https://github.com/user-attachments/assets/9f5b5c37-dacc-498e-80ca-255abe19bed5" /> |
| Outlined | <img width="400" alt="Trunk outlined 1" src="https://github.com/user-attachments/assets/3c7846b6-8f88-40cd-aa6e-5d212bafac7d" /> | <img width="400" alt="Feature outlined 1" src="https://github.com/user-attachments/assets/38fac809-cf08-4598-b87f-d02eaecdb637" /> |
| Animated | <img width="400" alt="Trunk animated 2" src="https://github.com/user-attachments/assets/1b476335-040d-47eb-95e4-7d2358b82f06" /> | <img width="400" alt="Feature animated 2" src="https://github.com/user-attachments/assets/69766923-8e91-43cb-b9b0-17fdae1d8c3d" /> | 
| Animated | <img width="400" alt="Trunk animated 1" src="https://github.com/user-attachments/assets/55ee98ed-0392-4b4c-a7b1-a8028a9b97ec" /> | <img width="400" alt="Feature animated 1" src="https://github.com/user-attachments/assets/425fe8aa-1446-4efa-8ce0-edd768103376" /> | 


Some observations/differences. In the feature branch:

- There is too much left padding/indentation in the controls in Outlined style. I think we can get rid of it. See [comment](https://github.com/Automattic/jetpack/pull/43327#discussion_r2092166338) 
- The options label (legend tag) hovers too far above the fieldset in Outlined style when other inputs have border radii, but the options don't. This one is weird.
- The button style option checkboxes are square. They are round in trunk

Minor things... Getting close!









author_association: MEMBER