Skip to content

Commit d349caa

Browse files
vianaswedanzer
andauthored
Forms: Add implicitly required single input form (#45300)
* Make single input required by default When a form has only one input field, we're making it required by default * Add form level required indicator settings I've added display settings for the required indicator badge. Options are: - Text (eg. "(required)") - Asterisk (*) - Hidden, only shows on form validation * Make the required asterisk red in the editor too * Hide required indicator in single-input forms If there's only one field, it's obvious it's required * changelog * Revert "Hide required indicator in single-input forms" This reverts commit 42edffb. * Revert "Make the required asterisk red in the editor too" This reverts commit cdf23bc. * Revert "Add form level required indicator settings" This reverts commit 64d2a56. * Move required indicator settings to the field level Also, use a radio buttons instead of a dropdown. * Make required indicator a sync attribute Now, all required fields will sync to the same value when sync is activated. * Fix required indicator multi-line field rendering The value of the required indicator wasn't being updated in the editor * Fix required indicator phone field rendering The value of the required indicator wasn't being updated in the editor * Fix tests * Update changelog * Simplify check for required field * Update required indictor control to toggle * Consolidate duplicate sync logic to hook * Fix phan issues * Sync required label across blocks * Fix more phan issues --------- Co-authored-by: Erick Danzer <[email protected]>
1 parent e4347aa commit d349caa

File tree

18 files changed

+277
-82
lines changed

18 files changed

+277
-82
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Forms: Added required indicator settings, made forms with a sinble input required by default.

projects/packages/forms/src/blocks/contact-form/edit.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,18 @@ const ALLOWED_FORM_BLOCKS = ALLOWED_BLOCKS.concat( CORE_BLOCKS ).filter(
101101

102102
const PRIORITIZED_INSERTER_BLOCKS = [ ...validFields.map( block => `jetpack/${ block.name }` ) ];
103103

104+
// Determine if a block has a required attribute. Exclude hidden fields.
105+
const isInputWithRequiredField = ( fullName?: string ): boolean => {
106+
if ( ! fullName || ! fullName.startsWith( 'jetpack/' ) ) return false;
107+
const baseName = fullName.slice( 'jetpack/'.length );
108+
const field = childBlocks.find( block => block.name === baseName );
109+
// @ts-expect-error: childBlocks are defined in JS without explicit types.
110+
// TS is inferring the type wrong. Fix is to update childBlocks to TS with types.
111+
const hasRequired = field && field?.settings?.attributes?.required !== undefined;
112+
const isHidden = field?.name === 'field-hidden';
113+
return hasRequired && ! isHidden;
114+
};
115+
104116
function JetpackContactFormEdit( { name, attributes, setAttributes, clientId, className } ) {
105117
// Initialize default form block settings as needed.
106118
useFormBlockDefaults( { attributes, setAttributes } );
@@ -219,7 +231,7 @@ function JetpackContactFormEdit( { name, attributes, setAttributes, clientId, cl
219231
const { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } =
220232
useModuleStatus( 'contact-form' );
221233

222-
const { replaceInnerBlocks, __unstableMarkNextChangeAsNotPersistent } =
234+
const { replaceInnerBlocks, __unstableMarkNextChangeAsNotPersistent, updateBlockAttributes } =
223235
useDispatch( blockEditorStore );
224236

225237
const currentInnerBlocks = useSelect(
@@ -230,6 +242,26 @@ function JetpackContactFormEdit( { name, attributes, setAttributes, clientId, cl
230242
// Track previous block count to detect insertions
231243
const previousBlockCountRef = useRef( currentInnerBlocks.length );
232244

245+
// Helper function to identify input field blocks
246+
const getInputFieldBlocks = useCallback( blocks => {
247+
const inputFields = [];
248+
249+
const findInputFields = blockList => {
250+
blockList.forEach( block => {
251+
if ( isInputWithRequiredField( block.name ) ) {
252+
inputFields.push( block );
253+
}
254+
// Recursively check inner blocks (for multistep forms)
255+
if ( block.innerBlocks && block.innerBlocks.length > 0 ) {
256+
findInputFields( block.innerBlocks );
257+
}
258+
} );
259+
};
260+
261+
findInputFields( blocks );
262+
return inputFields;
263+
}, [] );
264+
233265
// Effect to handle block insertion and reordering
234266
useEffect( () => {
235267
const currentBlockCount = currentInnerBlocks.length;
@@ -268,6 +300,22 @@ function JetpackContactFormEdit( { name, attributes, setAttributes, clientId, cl
268300
__unstableMarkNextChangeAsNotPersistent,
269301
] );
270302

303+
// Effect to automatically make single input fields required
304+
useEffect( () => {
305+
const inputFields = getInputFieldBlocks( currentInnerBlocks );
306+
307+
// Only proceed if there's exactly one input field
308+
if ( inputFields.length === 1 ) {
309+
const singleField = inputFields[ 0 ];
310+
311+
// Check if the field is not already required
312+
if ( ! singleField.attributes?.required ) {
313+
// Update the field to be required
314+
updateBlockAttributes( singleField.clientId, { required: true } );
315+
}
316+
}
317+
}, [ currentInnerBlocks, getInputFieldBlocks, updateBlockAttributes ] );
318+
271319
// Deep-scan helper – user might drop a Step block inside nested structures.
272320
const containsMultistepBlock = useCallback( function hasMultistep( blocks ) {
273321
return blocks.some(

projects/packages/forms/src/blocks/field-email/edit.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default function EmailFieldEdit( props ) {
1212
label={ __( 'Email', 'jetpack-forms' ) }
1313
required={ props.attributes.required }
1414
requiredText={ props.attributes.requiredText }
15+
requiredIndicator={ props.attributes.requiredIndicator }
1516
setAttributes={ props.setAttributes }
1617
isSelected={ props.isSelected }
1718
defaultValue={ props.attributes.defaultValue }

projects/packages/forms/src/blocks/field-name/edit.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default function NameFieldEdit( props ) {
1212
label={ __( 'Name', 'jetpack-forms' ) }
1313
required={ props.attributes.required }
1414
requiredText={ props.attributes.requiredText }
15+
requiredIndicator={ props.attributes.requiredIndicator }
1516
setAttributes={ props.setAttributes }
1617
isSelected={ props.isSelected }
1718
defaultValue={ props.attributes.defaultValue }

projects/packages/forms/src/blocks/field-number/edit.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default function NumberFieldEdit( props ) {
1212
label={ __( 'Number', 'jetpack-forms' ) }
1313
required={ props.attributes.required }
1414
requiredText={ props.attributes.requiredText }
15+
requiredIndicator={ props.attributes.requiredIndicator }
1516
setAttributes={ props.setAttributes }
1617
isSelected={ props.isSelected }
1718
defaultValue={ props.attributes.defaultValue }

projects/packages/forms/src/blocks/field-telephone/edit.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import JetpackFieldControls from '../shared/components/jetpack-field-controls';
2020
import useFieldSelected from '../shared/hooks/use-field-selected';
2121
import useFormWrapper from '../shared/hooks/use-form-wrapper';
2222
import useJetpackFieldStyles from '../shared/hooks/use-jetpack-field-styles';
23+
import useSyncRequiredIndicator from '../shared/hooks/use-sync-required-indicator';
2324
import { countries } from './country-list';
2425
import { getTranslatedCountryName } from './country-names-translated';
2526

@@ -40,6 +41,7 @@ export default function PhoneFieldEdit( props ) {
4041
placeholder,
4142
searchPlaceholder,
4243
default: defaultCountry,
44+
requiredIndicator,
4345
} = attributes;
4446
const [ countryList, setCountryList ] = useState( EMPTY_ARRAY );
4547

@@ -91,6 +93,7 @@ export default function PhoneFieldEdit( props ) {
9193
placeholder,
9294
required,
9395
requiredText,
96+
requiredIndicator,
9497
},
9598
],
9699
[ 'jetpack/phone-input', {} ],
@@ -99,6 +102,14 @@ export default function PhoneFieldEdit( props ) {
99102
__experimentalCaptureToolbars: true,
100103
} );
101104

105+
useSyncRequiredIndicator( {
106+
clientId,
107+
blockName: 'jetpack/field-sync',
108+
isSynced: attributes?.shareFieldAttributes,
109+
attributes,
110+
setAttributes,
111+
} );
112+
102113
// Handler is provided as context from edit as index.js can't pass it as a prop.
103114
const onChangeDefaultCountry = useCallback(
104115
event => {

projects/packages/forms/src/blocks/field-text/edit.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default function TextFieldEdit( props ) {
1212
label={ __( 'Text', 'jetpack-forms' ) }
1313
required={ props.attributes.required }
1414
requiredText={ props.attributes.requiredText }
15+
requiredIndicator={ props.attributes.requiredIndicator }
1516
setAttributes={ props.setAttributes }
1617
isSelected={ props.isSelected }
1718
defaultValue={ props.attributes.defaultValue }

projects/packages/forms/src/blocks/field-textarea/edit.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import JetpackFieldControls from '../shared/components/jetpack-field-controls';
66
import useFieldSelected from '../shared/hooks/use-field-selected';
77
import useFormWrapper from '../shared/hooks/use-form-wrapper';
88
import useJetpackFieldStyles from '../shared/hooks/use-jetpack-field-styles';
9+
import useSyncRequiredIndicator from '../shared/hooks/use-sync-required-indicator';
910
import { ALLOWED_INNER_BLOCKS } from '../shared/util/constants';
1011

1112
export default function TextareaFieldEdit( props ) {
1213
const { attributes, clientId, isSelected, setAttributes } = props;
13-
const { id, required, width } = attributes;
14+
const { id, required, width, requiredIndicator } = attributes;
1415

1516
useFormWrapper( props );
1617
const { blockStyle } = useJetpackFieldStyles( attributes );
@@ -25,17 +26,25 @@ export default function TextareaFieldEdit( props ) {
2526

2627
const template = useMemo( () => {
2728
return [
28-
[ 'jetpack/label', { label: __( 'Message', 'jetpack-forms' ) } ],
29+
[ 'jetpack/label', { label: __( 'Message', 'jetpack-forms' ), requiredIndicator } ],
2930
[ 'jetpack/input', { type: 'textarea' } ],
3031
];
31-
}, [] );
32+
}, [ requiredIndicator ] );
3233

3334
const innerBlocksProps = useInnerBlocksProps( blockProps, {
3435
allowedBlocks: ALLOWED_INNER_BLOCKS,
3536
template,
3637
templateLock: 'all',
3738
} );
3839

40+
useSyncRequiredIndicator( {
41+
clientId,
42+
blockName: 'jetpack/field-sync',
43+
isSynced: attributes?.shareFieldAttributes,
44+
attributes,
45+
setAttributes,
46+
} );
47+
3948
return (
4049
<>
4150
<div { ...innerBlocksProps } />

projects/packages/forms/src/blocks/field-time/edit.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default function NumberFieldEdit( props ) {
1212
label={ __( 'Time', 'jetpack-forms' ) }
1313
required={ props.attributes.required }
1414
requiredText={ props.attributes.requiredText }
15+
requiredIndicator={ props.attributes.requiredIndicator }
1516
setAttributes={ props.setAttributes }
1617
isSelected={ props.isSelected }
1718
defaultValue={ props.attributes.defaultValue }

projects/packages/forms/src/blocks/field-url/edit.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default function UrlFieldEdit( props ) {
1212
label={ __( 'Website', 'jetpack-forms' ) }
1313
required={ props.attributes.required }
1414
requiredText={ props.attributes.requiredText }
15+
requiredIndicator={ props.attributes.requiredIndicator }
1516
setAttributes={ props.setAttributes }
1617
isSelected={ props.isSelected }
1718
defaultValue={ props.attributes.defaultValue }

0 commit comments

Comments
 (0)