Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Response inspector: display star rating icons for rating field submissions.
13 changes: 9 additions & 4 deletions projects/packages/forms/src/blocks/field-rating/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
RangeControl,
} from '@wordpress/components';
import { useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import JetpackFieldControls from '../shared/components/jetpack-field-controls.js';
import useFormWrapper from '../shared/hooks/use-form-wrapper.js';
import { MAX_RATING_ICONS } from './rating-icons.js';

/**
* Rating Field Edit Component
Expand All @@ -25,7 +26,7 @@ export default function RatingFieldEdit( props ) {

// Direct update functions for rating attributes
const updateMax = newMax => {
const validatedMax = Math.min( Math.max( parseInt( newMax ) || 5, 2 ), 10 );
const validatedMax = Math.min( Math.max( parseInt( newMax ) || 5, 2 ), MAX_RATING_ICONS );
const validatedDefault = Math.min( defaultValue, validatedMax );
setAttributes( {
max: validatedMax,
Expand Down Expand Up @@ -91,9 +92,13 @@ export default function RatingFieldEdit( props ) {
<NumberControl
__next40pxDefaultSize
__unstableInputWidth="50%"
help={ __( 'Highest rating users can select (2–10).', 'jetpack-forms' ) }
help={ sprintf(
/* translators: %d: maximum rating value (e.g. 10) */
__( 'Highest rating users can select (2–%d).', 'jetpack-forms' ),
MAX_RATING_ICONS
) }
label={ __( 'Maximum rating', 'jetpack-forms' ) }
max={ 10 }
max={ MAX_RATING_ICONS }
min={ 2 }
onChange={ updateMax }
spinControls="custom"
Expand Down
32 changes: 32 additions & 0 deletions projects/packages/forms/src/blocks/field-rating/rating-icon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { SVG, Path } from '@wordpress/components';
import { RATING_ICONS } from './rating-icons.js';

/**
* Rating icon React component for use in the block editor and dashboard.
*
* @param {object} props - Component props.
* @param {string} props.iconStyle - The icon style ('stars' or 'hearts').
* @param {string} props.strokeColor - SVG stroke color.
* @param {string} props.fillColor - SVG fill color.
* @param {number} props.strokeWidth - SVG stroke width.
* @return {import('react').JSX.Element} SVG icon element.
*/
export const RatingIcon = ( {
iconStyle,
strokeColor = 'currentColor',
fillColor = 'none',
strokeWidth = 2,
} ) => {
const iconPath = RATING_ICONS[ iconStyle ] || RATING_ICONS.stars;
return (
<SVG className="jetpack-field-rating__icon" viewBox="0 0 24 24" aria-hidden="true">
<Path
d={ iconPath }
fill={ fillColor }
stroke={ strokeColor }
strokeWidth={ strokeWidth }
strokeLinejoin="round"
/>
</SVG>
);
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* Maximum number of rating icons to render.
* Used to prevent DOM bloat from malformed or excessively large values.
*/
export const MAX_RATING_ICONS = 10;

/**
* Rating icon SVG paths.
* Single source of truth for star and heart icon paths used across the forms package.
Expand Down
20 changes: 2 additions & 18 deletions projects/packages/forms/src/blocks/input-rating/edit.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useBlockProps } from '@wordpress/block-editor';
import { SVG, Path } from '@wordpress/components';
import { RATING_ICONS } from '../field-rating/rating-icons.js';
import { RatingIcon } from '../field-rating/rating-icon.js';
import useInsertAfterOnEnterKeyDown from '../shared/hooks/use-insert-after-on-enter-key-down.js';

export default function RatingInputEdit( { context, clientId } ) {
Expand All @@ -11,21 +10,6 @@ export default function RatingInputEdit( { context, clientId } ) {
const onKeyDown = useInsertAfterOnEnterKeyDown( clientId );

// Color and other support classes are injected by useBlockProps

// Get icon SVG based on iconStyle (default: stars)
const iconPath = RATING_ICONS[ iconStyle ] || RATING_ICONS.stars;
const iconSvg = (
<SVG className="jetpack-field-rating__icon" viewBox="0 0 24 24" aria-hidden="true">
<Path
d={ iconPath }
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
></Path>
</SVG>
);

const blockProps = useBlockProps( {
className: 'jetpack-field-rating__options',
} );
Expand All @@ -51,7 +35,7 @@ export default function RatingInputEdit( { context, clientId } ) {
onKeyDown={ onKeyDown }
/>
<label htmlFor={ radioId } className="jetpack-field-rating__label">
{ iconSvg }
<RatingIcon iconStyle={ iconStyle } />
</label>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import FieldEmail from '../field-email/index.tsx';
import FieldFile from '../field-file/index.tsx';
import FieldImageSelect from '../field-image-select/index.tsx';
import FieldPhone from '../field-phone/index.tsx';
import FieldRating from '../field-rating/index.tsx';
import { EMAIL_REGEX, getIconSource, inferFieldTypeFromLabel } from './field-preview-utils.ts';
import type { ResponseField, FieldType, FileItem } from '../../../../../types/index.ts';
import './style.scss';
Expand Down Expand Up @@ -175,6 +176,10 @@ const FieldPreview = ( { field, onFilePreview }: FieldPreviewProps ) => {
return <ExternalLink href={ stringValue }>{ stringValue }</ExternalLink>;
}

if ( fieldType === 'rating' ) {
return <FieldRating value={ stringValue } />;
}

return stringValue;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* External dependencies
*/
import {
__experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
VisuallyHidden,
} from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { RatingIcon } from '../../../../../blocks/field-rating/rating-icon.js';
import { MAX_RATING_ICONS } from '../../../../../blocks/field-rating/rating-icons.js';

type FieldRatingProps = {
value?: string | null;
};

const FieldRating = ( { value }: FieldRatingProps ) => {
const stringValue = value != null ? String( value ) : '';
if ( stringValue.trim() === '' ) {
return '-';
}
const [ rateValue, outOf ] = stringValue.split( '/' ) ?? [];
if ( ! rateValue || rateValue.trim() === '' ) {
return '-';
}
if ( ! outOf || outOf.trim() === '' ) {
return '-';
}

const rateValueTrimmed = rateValue.trim();
const outOfTrimmed = outOf.trim();

// Require strictly numeric values to reject partial matches like "4abc"
if ( ! /^[0-9]+$/.test( rateValueTrimmed ) || ! /^[0-9]+$/.test( outOfTrimmed ) ) {
return '-';
}

const parsedRating = Number.parseInt( rateValueTrimmed, 10 );
const parsedMax = Number.parseInt( outOfTrimmed, 10 );
if (
! Number.isFinite( parsedRating ) ||
parsedRating < 0 ||
! Number.isFinite( parsedMax ) ||
parsedMax < 0
) {
return '-';
}

// Clamp max to prevent DOM bloat from large values
const clampedMax = Math.min( parsedMax, MAX_RATING_ICONS );
const displayRating = Math.min( Math.max( 0, parsedRating ), clampedMax );

Comment on lines 42 to 54
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parsedMax of 0 currently renders an empty HStack (0 icons) with only a visually-hidden label. Consider treating max <= 0 as invalid and falling back to "-" to avoid an apparently blank value in the UI (e.g. input "0/0" or "3/0").

Copilot uses AI. Check for mistakes.
const ratingLabel = sprintf(
/* translators: 1: rating value, 2: maximum rating (e.g. "4" and "5" for "4 out of 5") */
__( 'Rating %1$s out of %2$s', 'jetpack-forms' ),
String( displayRating ),
String( clampedMax )
Comment on lines +55 to +59
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The accessible label uses displayRating/clampedMax, which can become inaccurate when the submitted value has a larger max than MAX_RATING_ICONS (e.g. "50/100" will be announced as "Rating 10 out of 10"). Keep clamping only for rendered icon count, but use the original parsed values (or otherwise convey the cap) in the VisuallyHidden text so screen readers get the real rating.

Suggested change
const ratingLabel = sprintf(
/* translators: 1: rating value, 2: maximum rating (e.g. "4" and "5" for "4 out of 5") */
__( 'Rating %1$s out of %2$s', 'jetpack-forms' ),
String( displayRating ),
String( clampedMax )
// Use the original scale in the accessible label so screen readers get the real rating.
const accessibleRating = Math.min( Math.max( 0, parsedRating ), parsedMax );
const ratingLabel = sprintf(
/* translators: 1: rating value, 2: maximum rating (e.g. "4" and "5" for "4 out of 5") */
__( 'Rating %1$s out of %2$s', 'jetpack-forms' ),
String( accessibleRating ),
String( parsedMax )

Copilot uses AI. Check for mistakes.
);

return (
<>
<VisuallyHidden as="span">{ ratingLabel }</VisuallyHidden>
<HStack spacing="1" alignment="topLeft">
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alignment value "topLeft" should be "left" for HStack. The WordPress HStack component accepts alignment values like "left", "center", "right", "top", "bottom" but not "topLeft". This may cause unexpected behavior or styling issues.

Suggested change
<HStack spacing="1" alignment="topLeft">
<HStack spacing="1" alignment="left">

Copilot uses AI. Check for mistakes.
{ Array.from( { length: clampedMax }, ( _, index ) => (
<span style={ { flex: '0 0 24px' } } key={ index }>
<RatingIcon
iconStyle="stars"
strokeColor={ index < displayRating ? '#F0B849' : '#757575' }
fillColor={ index < displayRating ? '#F0B849' : 'none' }
strokeWidth={ 1.5 }
/>
</span>
) ) }
</HStack>
</>
);
};

export default FieldRating;
Loading
Loading