Skip to content

Commit 70ad145

Browse files
authored
Forms: UX improvements for Image Select field (#45516)
* fix scroll on inner image block placeholder * auto-adjust the image resolution when changing the supersize attribute * add explicit default style values * add helper text to settings * remove default numbered labels * change option label placeholder * add option on enter and prevent newlines on option label * changelog
1 parent c02c347 commit 70ad145

File tree

9 files changed

+174
-66
lines changed

9 files changed

+174
-66
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: changed
3+
4+
Forms: UX improvements for Image Select field

projects/packages/forms/src/blocks/field-image-select/edit.tsx

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import {
88
BlockControls,
99
} from '@wordpress/block-editor';
1010
import { ToggleControl, ToolbarButton, ToolbarGroup } from '@wordpress/components';
11-
import { useSelect } from '@wordpress/data';
12-
import { useMemo } from '@wordpress/element';
11+
import { store as coreStore } from '@wordpress/core-data';
12+
import { useDispatch, useSelect, select as globalSelect } from '@wordpress/data';
13+
import { useCallback, useMemo } from '@wordpress/element';
1314
import { __ } from '@wordpress/i18n';
1415
import clsx from 'clsx';
1516
/**
@@ -25,25 +26,53 @@ import './editor.scss';
2526
* Types
2627
*/
2728
import type { Block, BlockEditorStoreSelect } from '../../types';
29+
import type { Attachment } from '@wordpress/core-data';
2830

2931
export default function ImageSelectFieldEdit( props ) {
3032
const { attributes, clientId, setAttributes, name } = props;
3133
const { id, required, width } = attributes;
34+
const { updateBlockAttributes } = useDispatch( blockEditorStore );
3235
const { blockStyle } = useJetpackFieldStyles( attributes );
3336

34-
const { optionsBlock } = useSelect(
37+
const { optionsBlock, imagesData } = useSelect(
3538
select => {
3639
const { getBlock } = select( blockEditorStore ) as BlockEditorStoreSelect;
3740

41+
const block = getBlock( clientId )?.innerBlocks.find(
42+
( innerBlock: Block ) => innerBlock.name === 'jetpack/fieldset-image-options'
43+
);
44+
45+
const images =
46+
block?.innerBlocks?.[ 0 ]?.innerBlocks
47+
// Filter out inner blocks that don't have a media id, i.e. external images.
48+
?.filter( innerBlock => innerBlock.attributes?.id !== undefined )
49+
// Map the inner blocks to an array of objects with the media id and client id.
50+
?.map( innerBlock => ( {
51+
clientId: innerBlock.clientId,
52+
mediaId: innerBlock.attributes.id as number,
53+
} ) ) ?? [];
54+
3855
return {
39-
optionsBlock: getBlock( clientId )?.innerBlocks.find(
40-
( block: Block ) => block.name === 'jetpack/fieldset-image-options'
41-
),
56+
optionsBlock: block,
57+
imagesData: images,
4258
};
4359
},
4460
[ clientId ]
4561
);
4662

63+
// Preload the image entity records reactively, as they are not available on first load.
64+
// This is necessary to ensure the image URLs can be updated correctly when the supersized attribute is changed.
65+
useSelect(
66+
select => {
67+
return imagesData.map( image =>
68+
select( coreStore ).getEntityRecord( 'postType', 'attachment', image.mediaId, {
69+
context: 'view',
70+
} )
71+
);
72+
},
73+
[ imagesData ]
74+
);
75+
4776
// This wraps the field in a form block if it is added directly to the editor.
4877
useFormWrapper( { attributes, clientId, name } );
4978

@@ -83,6 +112,44 @@ export default function ImageSelectFieldEdit( props ) {
83112
}
84113
);
85114

115+
const updateSupersized = useCallback(
116+
( value: boolean ) => {
117+
setAttributes( { isSupersized: value } );
118+
119+
const inputImageOptions = optionsBlock?.innerBlocks;
120+
121+
if ( inputImageOptions && inputImageOptions.length > 0 ) {
122+
const imageBlocks = inputImageOptions.map( ( block: Block ) => block.innerBlocks[ 0 ] );
123+
const newSizeSlug = value ? 'full' : 'medium';
124+
125+
imageBlocks.forEach( imageBlock => {
126+
updateBlockAttributes( imageBlock.clientId, {
127+
sizeSlug: newSizeSlug,
128+
} );
129+
130+
const record = globalSelect( coreStore ).getEntityRecord(
131+
'postType',
132+
'attachment',
133+
imageBlock.attributes.id as number,
134+
{
135+
context: 'view',
136+
}
137+
);
138+
139+
const newUrl = ( record as Attachment )?.media_details?.sizes?.[ newSizeSlug ]
140+
?.source_url;
141+
142+
if ( newUrl ) {
143+
updateBlockAttributes( imageBlock.clientId, {
144+
url: newUrl,
145+
} );
146+
}
147+
} );
148+
}
149+
},
150+
[ setAttributes, optionsBlock?.innerBlocks, updateBlockAttributes ]
151+
);
152+
86153
return (
87154
<div { ...blockProps }>
88155
<div { ...innerBlocksProps } />
@@ -111,6 +178,10 @@ export default function ImageSelectFieldEdit( props ) {
111178
label={ __( 'Show labels', 'jetpack-forms' ) }
112179
checked={ attributes?.showLabels }
113180
onChange={ ( value: boolean ) => setAttributes( { showLabels: value } ) }
181+
help={ __(
182+
'Displays the labels for the images in the published form. They are always visible for you in the editor and in the responses.',
183+
'jetpack-forms'
184+
) }
114185
/>
115186
),
116187
},
@@ -122,7 +193,8 @@ export default function ImageSelectFieldEdit( props ) {
122193
key="is-supersized"
123194
label={ __( 'Supersized', 'jetpack-forms' ) }
124195
checked={ attributes?.isSupersized }
125-
onChange={ ( value: boolean ) => setAttributes( { isSupersized: value } ) }
196+
onChange={ ( value: boolean ) => updateSupersized( value ) }
197+
help={ __( 'Changes the size of the images.', 'jetpack-forms' ) }
126198
/>
127199
),
128200
},
@@ -135,6 +207,7 @@ export default function ImageSelectFieldEdit( props ) {
135207
label={ __( 'Multiple selection', 'jetpack-forms' ) }
136208
checked={ attributes?.isMultiple }
137209
onChange={ ( value: boolean ) => setAttributes( { isMultiple: value } ) }
210+
help={ __( 'Allows visitors to select more than one image.', 'jetpack-forms' ) }
138211
/>
139212
),
140213
},
@@ -147,6 +220,10 @@ export default function ImageSelectFieldEdit( props ) {
147220
label={ __( 'Randomize', 'jetpack-forms' ) }
148221
checked={ attributes?.randomizeOptions }
149222
onChange={ ( value: boolean ) => setAttributes( { randomizeOptions: value } ) }
223+
help={ __(
224+
'Randomizes the order of the images in the published form to avoid order bias. This setting does not affect the order in the editor.',
225+
'jetpack-forms'
226+
) }
150227
/>
151228
),
152229
},

projects/packages/forms/src/blocks/field-image-select/editor.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
flex-grow: 0;
1414
}
1515

16+
.jetpack-contact-form .jetpack-input-image-option .components-placeholder.has-illustration {
17+
overflow: hidden;
18+
}
19+
1620
// There is no outer block in the editor, so we apply
1721
// the styles directly to the input image option.
1822
.jetpack-input-image-option {

projects/packages/forms/src/blocks/field-image-select/index.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { __ } from '@wordpress/i18n';
55
/**
66
* Internal dependencies
77
*/
8-
import { getImageOptionLabel } from '../input-image-option/label';
98
import defaultSettings from '../shared/settings';
109
import edit from './edit';
1110
import icon from './icon';
@@ -72,9 +71,6 @@ const settings = {
7271
innerBlocks: [
7372
{
7473
name: 'jetpack/input-image-option',
75-
attributes: {
76-
label: getImageOptionLabel( 1 ),
77-
},
7874
innerBlocks: [
7975
{
8076
name: 'core/image',
@@ -88,9 +84,6 @@ const settings = {
8884
},
8985
{
9086
name: 'jetpack/input-image-option',
91-
attributes: {
92-
label: getImageOptionLabel( 2 ),
93-
},
9487
innerBlocks: [
9588
{
9689
name: 'core/image',

projects/packages/forms/src/blocks/fieldset-image-options/edit.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import clsx from 'clsx';
99
/**
1010
* Internal dependencies
1111
*/
12-
import { getImageOptionLabel } from '../input-image-option/label';
1312
import useAddImageOption from '../shared/hooks/use-add-image-option';
1413
import useJetpackFieldStyles from '../shared/hooks/use-jetpack-field-styles';
1514

@@ -26,9 +25,9 @@ export default function ImageOptionsFieldsetEdit( props ) {
2625

2726
// Starts with 3 empty options.
2827
const template = [
29-
[ 'jetpack/input-image-option', { label: getImageOptionLabel( 1 ) } ],
30-
[ 'jetpack/input-image-option', { label: getImageOptionLabel( 2 ) } ],
31-
[ 'jetpack/input-image-option', { label: getImageOptionLabel( 3 ) } ],
28+
[ 'jetpack/input-image-option' ],
29+
[ 'jetpack/input-image-option' ],
30+
[ 'jetpack/input-image-option' ],
3231
];
3332

3433
const defaultBlock = useMemo( () => newImageOption(), [ newImageOption ] );

projects/packages/forms/src/blocks/input-image-option/edit.tsx

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import {
88
RichText,
99
} from '@wordpress/block-editor';
1010
import { useSelect } from '@wordpress/data';
11-
import { useMemo } from '@wordpress/element';
11+
import { useMemo, useCallback } from '@wordpress/element';
1212
import { __ } from '@wordpress/i18n';
1313
import clsx from 'clsx';
1414
/**
1515
* Internal dependencies
1616
*/
17+
import useAddImageOption from '../shared/hooks/use-add-image-option';
1718
import useJetpackFieldStyles from '../shared/hooks/use-jetpack-field-styles';
1819
import { useSyncedAttributes } from '../shared/hooks/use-synced-attributes';
1920
import { getImageOptionLetter } from './label';
@@ -40,7 +41,7 @@ export default function ImageOptionInputEdit( props ) {
4041

4142
const { 'jetpack/field-image-select-is-supersized': isSupersized } = context || {};
4243

43-
const { positionLetter, rowOptionsCount } = useSelect(
44+
const { positionIndex, positionLetter, rowOptionsCount, parentId } = useSelect(
4445
select => {
4546
const blockEditor = select( blockEditorStore ) as BlockEditorStoreSelect;
4647
const { getBlock } = blockEditor;
@@ -49,8 +50,8 @@ export default function ImageOptionInputEdit( props ) {
4950
clientId,
5051
'jetpack/fieldset-image-options'
5152
);
52-
const parentId = parentClientIds[ parentClientIds.length - 1 ];
53-
const parentBlock = getBlock( parentId );
53+
const parentBlockId = parentClientIds[ parentClientIds.length - 1 ];
54+
const parentBlock = getBlock( parentBlockId );
5455

5556
// Find position within parent's inner blocks
5657
const position =
@@ -63,13 +64,43 @@ export default function ImageOptionInputEdit( props ) {
6364
const rowSiblingCount = Math.min( totalOptionsCount, maxImagesPerRow );
6465

6566
return {
67+
positionIndex: position,
6668
positionLetter: getImageOptionLetter( position ),
6769
rowOptionsCount: rowSiblingCount,
70+
parentId: parentBlockId,
6871
};
6972
},
7073
[ clientId, isSupersized ]
7174
);
7275

76+
const { addOption } = useAddImageOption( parentId );
77+
78+
// Handle key events to prevent newlines and add new option on Enter
79+
const handleKeyDown = useCallback(
80+
( event: KeyboardEvent ) => {
81+
if ( event.key === 'Enter' ) {
82+
event.preventDefault();
83+
addOption( positionIndex );
84+
}
85+
},
86+
[ addOption, positionIndex ]
87+
);
88+
89+
// Filter pasted content to remove newlines
90+
const handlePaste = useCallback(
91+
( event: ClipboardEvent ) => {
92+
event.preventDefault();
93+
94+
const pastedText = event.clipboardData?.getData( 'text/plain' ) || '';
95+
const cleanText = pastedText.replace( /[\r\n]+/g, ' ' ).trim();
96+
97+
if ( cleanText ) {
98+
setAttributes( { label: cleanText } );
99+
}
100+
},
101+
[ setAttributes ]
102+
);
103+
73104
// Use the block's own synced attributes for styling
74105
const { blockStyle } = useJetpackFieldStyles( attributes );
75106

@@ -90,10 +121,11 @@ export default function ImageOptionInputEdit( props ) {
90121
{
91122
scale: 'cover',
92123
aspectRatio: '1', // Square aspect ratio for uniform grid
124+
sizeSlug: isSupersized ? 'full' : 'medium',
93125
},
94126
],
95127
];
96-
}, [] );
128+
}, [ isSupersized ] );
97129

98130
const innerBlocksProps = useInnerBlocksProps(
99131
{ className: 'jetpack-input-image-option__wrapper' },
@@ -113,9 +145,11 @@ export default function ImageOptionInputEdit( props ) {
113145
tagName="span"
114146
className="jetpack-input-image-option__label"
115147
value={ label }
116-
placeholder={ __( 'Add option…', 'jetpack-forms' ) }
148+
placeholder={ __( 'Add label', 'jetpack-forms' ) }
117149
__unstableDisableFormats
118150
onChange={ ( newLabel: string ) => setAttributes( { label: newLabel } ) }
151+
onKeyDown={ handleKeyDown }
152+
onPaste={ handlePaste }
119153
/>
120154
</div>
121155
</div>

projects/packages/forms/src/blocks/input-image-option/index.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,23 @@ const settings = {
9090
type: 'string',
9191
default: '',
9292
},
93+
style: {
94+
type: 'object',
95+
default: {
96+
border: {
97+
radius: '4px',
98+
width: '1px',
99+
},
100+
spacing: {
101+
padding: {
102+
top: '8px',
103+
right: '8px',
104+
bottom: '8px',
105+
left: '8px',
106+
},
107+
},
108+
},
109+
},
93110
},
94111
save,
95112
};
Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
/**
2-
* External dependencies
3-
*/
4-
import { __, sprintf } from '@wordpress/i18n';
5-
61
/**
72
* Generates a letter-based label for image option fields.
83
* Converts position to letters: 1=A, 2=B, ..., 26=Z, 27=AA, 28=AB, etc.
@@ -23,17 +18,3 @@ export const getImageOptionLetter = ( position: number ): string => {
2318

2419
return result;
2520
};
26-
27-
/**
28-
* Generates a translated label for image option fields.
29-
*
30-
* @param {number} index - The 1-based index of the image option.
31-
* @return {string} The translated label for the image option.
32-
*/
33-
export const getImageOptionLabel = ( index: number ): string => {
34-
return sprintf(
35-
// translators: %d is the number of the choice, e.g. "Choice 1".
36-
__( 'Choice %d', 'jetpack-forms' ),
37-
index
38-
);
39-
};

0 commit comments

Comments
 (0)