Skip to content

Conversation

@enejb
Copy link
Member

@enejb enejb commented Jan 6, 2026

Resolves FORMS-136

Introduces an 'Other' option for radio fields, allowing users to enter custom text. Includes editor UI controls, ARIA accessibility, validation, metadata storage, frontend handling, and comprehensive tests for correct processing and storage of 'Other' responses.

See

Screen.Recording.2026-01-06.at.9.52.24.AM.mov

Proposed changes:

  • Add 'Other' option support for radio fields, allowing users to provide custom text input when selecting "Other"
  • Implement ARIA accessibility attributes (aria-label, aria-describedby) for screen reader support
  • Add automatic focus management - when "Other" is selected, the text input receives focus
  • Add input clearing behavior - when switching away from "Other", the text input is automatically cleared
  • Implement proper validation to ensure custom text is provided when "Other" is selected and field is required
  • Store metadata (is_other_option, other_label, other_user_value) for flexible display in different formats
  • Add comprehensive test coverage for form submission with separate radio and text input values

Other information:

  • Have you written new tests for your changes, if applicable?
  • Have you checked the E2E test CI results, and verified that your changes do not break them?
  • Have you tested your changes on WordPress.com, if applicable (if so, you'll see a generated comment below with a script to run)?

Jetpack product discussion

Does this pull request change what data or activity we track or use?

No

Testing instructions:

  • Set up a WordPress site with Jetpack Forms installed
  • Create a new post or page and add a Jetpack Form block
  • Add a "Single Choice" (radio) field to the form
  • In the field settings, add several options (e.g., "Red", "Blue", "Green")
  • For one of the options, enable the "Allow custom text input" toggle (this marks it as the "Other" option)
  • Publish the page and view it on the frontend
  • Test the "Other" option functionality:
    • Select the "Other" option - verify the text input appears and receives focus
    • Enter some custom text (e.g., "Purple with green stripes")
    • Submit the form
    • Verify the submission shows both the "Other" label and your custom text (e.g., "Other: Purple with green stripes")
  • Test switching behavior:
    • Select the "Other" option and enter some text
    • Select a different radio option
    • Verify the text input is cleared
    • Select "Other" again and verify the input is empty
  • Test validation:
    • Mark the field as required in the field settings
    • Select the "Other" option but leave the text input empty
    • Submit the form
    • Verify a validation error appears requiring custom text to be entered
  • Test accessibility:
    • Use a screen reader to verify the "Other" text input has proper ARIA labels
    • Verify the input is announced as "Other (please specify)" or similar
  • Run the test suite: vendor/bin/phpunit --filter test_radio_field_with_other_option
  • Verify the test passes, confirming proper handling of the two-field submission pattern"--base trunk

@enejb enejb requested review from a team and Copilot January 6, 2026 17:55
@enejb enejb added [Type] Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Status] Needs Review This PR is ready for review. [Package] Forms labels Jan 6, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 6, 2026

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add a "[Type]" label (Bug, Enhancement, Janitorial, Task).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

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 🤖


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 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: February 3, 2026
    • Code freeze: February 3, 2026

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

@enejb
Copy link
Member Author

enejb commented Jan 6, 2026

Some UI questions.

  1. Do we show the text input in the editor?
  2. How do users edit the placeholder for other?

Todo/check.

  • How does this look like when you export this (csv), email
  • How does it look like when you apply different form "styles" to it.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 6, 2026

Are you an Automattician? The PR will need to be tested on WordPress.com. This comment will be updated with testing instructions as soon the build is complete.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds "Other" option support to radio fields in Jetpack Forms, enabling users to provide custom text when selecting an "Other" option. The implementation spans frontend interactivity, backend processing, validation, ARIA accessibility, and comprehensive test coverage.

Key Changes:

  • Adds editor UI controls for enabling/configuring "Other" options on radio fields
  • Implements frontend handling with automatic focus management and input clearing
  • Adds backend validation to ensure custom text is provided when "Other" is selected on required fields
  • Stores metadata (is_other_option, other_label, other_user_value) for flexible rendering

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
Feedback_Fields_Test.php Adds comprehensive test for "Other" option form submission and storage
view.js Implements frontend state management and event handlers for "Other" option interaction
grunion.scss Adds styling for "Other" text input with visibility control and disabled states
class-feedback.php Adds processing logic to detect and extract "Other" option metadata from submissions
class-contact-form-plugin.php Preserves isOther attribute from option blocks for server-side rendering
class-contact-form-field.php Implements validation and HTML rendering for "Other" text input with ARIA attributes
settings/index.js Adds allowOther and otherLabel block attributes
option/index.js Adds isOther attribute to option blocks
option/edit.js Adds toolbar button to mark options as "Other" with visual styling
field-single-choice/edit.js Adds toggle control to enable/disable "Other" option with automatic option block management
editor.scss Adds italic styling for "Other" options in the editor
changelog Documents the new feature addition

Comment on lines 1362 to 1365
$field .= "<input id='" . esc_attr( $radio_id ) . "' type='radio' name='" . esc_attr( $id ) . "' value='" . esc_attr( $radio_value ) . "' data-wp-on--change='actions.onOtherRadioChange' data-other-label='" . esc_attr( $option_label ) . "' " . $class . checked( $option_label, $value, false ) . ' ' . ( $required ? "required aria-required=\'true\'" : '' ) . '/> ';
$has_option_is_other_id = esc_attr( $radio_id );
} else {
$field .= "<input id='" . esc_attr( $radio_id ) . "' type='radio' name='" . esc_attr( $id ) . "' value='" . esc_attr( $radio_value ) . "' data-wp-on--change='actions.onFieldChange' " . $class . checked( $option_label, $value, false ) . ' ' . ( $required ? "required aria-required=\'true\'" : '' ) . '/> ';
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The lines 1362 and 1365 are extremely long (over 200 characters each), making them difficult to read and maintain. Consider breaking these HTML concatenations into multiple lines for better readability.

Suggested change
$field .= "<input id='" . esc_attr( $radio_id ) . "' type='radio' name='" . esc_attr( $id ) . "' value='" . esc_attr( $radio_value ) . "' data-wp-on--change='actions.onOtherRadioChange' data-other-label='" . esc_attr( $option_label ) . "' " . $class . checked( $option_label, $value, false ) . ' ' . ( $required ? "required aria-required=\'true\'" : '' ) . '/> ';
$has_option_is_other_id = esc_attr( $radio_id );
} else {
$field .= "<input id='" . esc_attr( $radio_id ) . "' type='radio' name='" . esc_attr( $id ) . "' value='" . esc_attr( $radio_value ) . "' data-wp-on--change='actions.onFieldChange' " . $class . checked( $option_label, $value, false ) . ' ' . ( $required ? "required aria-required=\'true\'" : '' ) . '/> ';
$field .= "<input id='" . esc_attr( $radio_id ) . "' "
. "type='radio' name='" . esc_attr( $id ) . "' "
. "value='" . esc_attr( $radio_value ) . "' "
. "data-wp-on--change='actions.onOtherRadioChange' "
. "data-other-label='" . esc_attr( $option_label ) . "' "
. $class
. checked( $option_label, $value, false )
. ' '
. ( $required ? "required aria-required=\'true\'" : '' )
. '/> ';
$has_option_is_other_id = esc_attr( $radio_id );
} else {
$field .= "<input id='" . esc_attr( $radio_id ) . "' "
. "type='radio' name='" . esc_attr( $id ) . "' "
. "value='" . esc_attr( $radio_value ) . "' "
. "data-wp-on--change='actions.onFieldChange' "
. $class
. checked( $option_label, $value, false )
. ' '
. ( $required ? "required aria-required=\'true\'" : '' )
. '/> ';

Copilot uses AI. Check for mistakes.
id='" . $other_text_id . "'
name='" . esc_attr( $id ) . "-other-text'
type='text'
class='grunion-field'
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The class 'grunion-field' is applied to the "Other" text input but should likely include 'jetpack-other-text-input' as well for consistency with the wrapper's class name and the CSS rules defined for this element.

Suggested change
class='grunion-field'
class='grunion-field jetpack-other-text-input'

Copilot uses AI. Check for mistakes.
Comment on lines 78 to 89
<BlockControls>
<ToolbarGroup>
<ToolbarButton
onClick={ () => setAttributes( { isOther: ! isOther } ) }
className={ isOther ? 'is-pressed' : undefined }
>
{ __( 'Other', 'jetpack-forms' ) }
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The "Other" toolbar button is shown for all field types (checkbox, radio) but the functionality is only implemented for radio fields. This could confuse users who see the button on non-radio field options where it won't work. Consider conditionally displaying the toolbar button only for radio field types.

Suggested change
<BlockControls>
<ToolbarGroup>
<ToolbarButton
onClick={ () => setAttributes( { isOther: ! isOther } ) }
className={ isOther ? 'is-pressed' : undefined }
>
{ __( 'Other', 'jetpack-forms' ) }
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
{ type === 'radio' && (
<BlockControls>
<ToolbarGroup>
<ToolbarButton
onClick={ () => setAttributes( { isOther: ! isOther } ) }
className={ isOther ? 'is-pressed' : undefined }
>
{ __( 'Other', 'jetpack-forms' ) }
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
) }

Copilot uses AI. Check for mistakes.
Comment on lines +679 to +748
public function test_radio_field_with_other_option() {
$form_id = Utility::get_form_id();

// Create form submission with separate radio value and text input value
// This simulates the actual POST data from the form
$_post_data = Utility::get_post_request(
array(
'favoritecolor' => 'Other', // Radio button value
'favoritecolor-other-text' => 'Purple with green stripes', // Text input value
),
'g' . $form_id
);

// Create options data with an "Other" option
$optionsdata = Contact_Form::esc_shortcode_val(
wp_json_encode(
array(
array(
'label' => 'Red',
),
array(
'label' => 'Blue',
),
array(
'label' => 'Other',
'isOther' => true,
),
),
JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
)
);

$shortcode = "[contact-field type='radio' label='Favorite Color' allowOther='1' options='Red,Blue,Other' optionsdata='{$optionsdata}' /]";

$form = new Contact_Form(
array(
'title' => 'Test Form',
),
$shortcode
);

$response = Feedback::from_submission( $_post_data, $form );
$feedback_post_id = $response->save();
$saved_response = Feedback::get( $feedback_post_id );

// Get the field
$fields = $response->get_fields();
$this->assertNotEmpty( $fields, 'Fields should not be empty' );

$field = reset( $fields ); // Get first field
$this->assertInstanceOf( Feedback_Field::class, $field, 'Field should be a Feedback_Field instance' );

// Test that the value is preserved
$this->assertEquals( 'Other: Purple with green stripes', $field->get_value(), 'Value should be preserved as submitted' );

// Test that metadata is set correctly
$meta = $field->get_meta();
$this->assertTrue( $meta['is_other_option'], 'is_other_option should be true' );
$this->assertEquals( 'Other', $meta['other_label'], 'other_label should be "Other"' );
$this->assertEquals( 'Purple with green stripes', $meta['other_user_value'], 'other_user_value should contain custom text' );

// Test saved response
$saved_fields = $saved_response->get_fields();
$saved_field = reset( $saved_fields );

$this->assertEquals( 'Other: Purple with green stripes', $saved_field->get_value(), 'Saved value should match' );
$saved_meta = $saved_field->get_meta();
$this->assertTrue( $saved_meta['is_other_option'], 'Saved is_other_option should be true' );
$this->assertEquals( 'Purple with green stripes', $saved_meta['other_user_value'], 'Saved other_user_value should match' );
}
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The test doesn't cover the validation scenario where the "Other" option is selected on a required field but no custom text is provided. This is a critical validation path mentioned in the PR description and implemented in the validation logic (lines 434-438 of class-contact-form-field.php). Consider adding a test case that verifies the validation error is triggered when custom text is missing.

Copilot uses AI. Check for mistakes.
Comment on lines +416 to +417
field.isOtherSelected = true;
field.otherLabel = otherLabel;
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The code directly mutates the field object properties (field.isOtherSelected, field.otherLabel) rather than using a proper state update mechanism. This breaks immutability principles and could lead to issues with state management, reactivity, and debugging. Consider using a proper state update function or immutable update patterns.

Copilot uses AI. Check for mistakes.
/* "Other" option text input styles */
.jetpack-other-text-input-wrapper {
display: none;
margin-left: 1.5em;
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The CSS uses physical properties (margin-left) instead of logical properties (margin-inline-start). For RTL language support and better internationalization, consider using logical properties as per the project's coding guidelines.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines 128 to 140
<BlockControls>
<ToolbarGroup>
<ToolbarButton
onClick={ () => setAttributes( { isOther: ! isOther } ) }
className={ isOther ? 'is-pressed' : undefined }
>
{ __( 'Other', 'jetpack-forms' ) }
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The "Other" toolbar button is shown for all field types (checkbox, radio) but the functionality is only implemented for radio fields. This could confuse users who see the button on non-radio field options where it won't work. Consider conditionally displaying the toolbar button only for radio field types.

Copilot uses AI. Check for mistakes.
" . $other_input_styles . "
placeholder='" . esc_attr( __( 'Please specify...', 'jetpack-forms' ) ) . "'
value=''
aria-labelledby='" . $other_label_id . "'
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The text input has aria-labelledby pointing to a label with class 'screen-reader-text', but the input is inside a region that's hidden (display: none) until the "Other" option is selected. When the region becomes visible with aria-live='polite', the screen reader might not properly announce the input's purpose. Consider adding an aria-describedby attribute that references the parent radio button's label to provide better context about what option this text input relates to.

Suggested change
aria-labelledby='" . $other_label_id . "'
aria-labelledby='" . $other_label_id . "'
aria-describedby='" . esc_attr( $has_option_is_other_id ) . "'

Copilot uses AI. Check for mistakes.
Comment on lines +74 to +82
// For radio fields, check if value uses an "Other" pattern
let isOtherSelected = false;
let otherLabel = null;
if ( type === 'radio' && value && value.includes( ': ' ) ) {
// Check if this might be an "Other" value (has the pattern "Label: text")
const colonIndex = value.indexOf( ': ' );
const possibleLabel = value.substring( 0, colonIndex );
// Simple heuristic: if the label part is short (< 30 chars), it might be an "Other" label
if ( possibleLabel.length < 30 ) {
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The heuristic for detecting "Other" options in radio fields is fragile. The code checks if a value contains ": " and if the label part is less than 30 characters to determine if it's an "Other" option. This could lead to false positives - any radio option value that happens to contain a colon followed by text with a short prefix would be incorrectly identified as an "Other" option. Consider using a more reliable mechanism, such as explicitly checking against known "Other" labels from the field configuration or using a metadata flag.

Suggested change
// For radio fields, check if value uses an "Other" pattern
let isOtherSelected = false;
let otherLabel = null;
if ( type === 'radio' && value && value.includes( ': ' ) ) {
// Check if this might be an "Other" value (has the pattern "Label: text")
const colonIndex = value.indexOf( ': ' );
const possibleLabel = value.substring( 0, colonIndex );
// Simple heuristic: if the label part is short (< 30 chars), it might be an "Other" label
if ( possibleLabel.length < 30 ) {
// For radio fields, check if value uses an explicit "Other" pattern
let isOtherSelected = false;
let otherLabel = null;
if ( type === 'radio' && typeof value === 'string' && value.includes( ': ' ) ) {
const colonIndex = value.indexOf( ': ' );
const possibleLabel = value.substring( 0, colonIndex );
// Only treat as "Other" when the label explicitly matches a known "Other" option.
if ( possibleLabel === 'Other' ) {

Copilot uses AI. Check for mistakes.
Comment on lines +391 to +393
field.isOtherSelected = false;
field.otherLabel = null;

Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The code directly mutates the field object properties (field.isOtherSelected, field.otherLabel) rather than using a proper state update mechanism. This breaks immutability principles and could lead to issues with state management, reactivity, and debugging. Consider using a proper state update function or immutable update patterns.

Suggested change
field.isOtherSelected = false;
field.otherLabel = null;
const updatedField = {
...field,
isOtherSelected: false,
otherLabel: null,
};
context.fields = {
...context.fields,
[ fieldId ]: updatedField,
};

Copilot uses AI. Check for mistakes.
@jp-launch-control
Copy link

jp-launch-control bot commented Jan 6, 2026

Code Coverage Summary

Coverage changed in 7 files. Only the first 5 are listed here.

File Coverage Δ% Δ Uncovered
projects/packages/forms/src/modules/form/view.js 0/325 (0.00%) 0.00% 45 💔
projects/packages/forms/src/contact-form/class-contact-form-field.php 1092/1781 (61.31%) -1.30% 37 💔
projects/packages/forms/src/blocks/field-single-choice/edit.js 0/32 (0.00%) 0.00% 22 💔
projects/packages/forms/src/blocks/option/edit.js 0/44 (0.00%) 0.00% 13 💔
projects/packages/forms/src/contact-form/class-feedback.php 690/728 (94.78%) -0.88% 8 💔

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR I don't care about code coverage for this PR Use this label to ignore the check for insufficient code coveage.

@simison simison force-pushed the add/other-option-to-dropdown branch from 3085195 to 0d958f6 Compare January 7, 2026 10:32
},
otherLabel: {
type: 'string',
default: 'Other',
Copy link
Member

Choose a reason for hiding this comment

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

Needs __()

Comment on lines 23 to 30
allowOther: {
type: 'boolean',
default: false,
},
otherLabel: {
type: 'string',
default: 'Other',
},
Copy link
Member

Choose a reason for hiding this comment

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

Wondering if we could simplify by not having other options at the field level, and only have isOther at the inner option block? Once that's present, the other text field gets rendered right below it?

Comment on lines 1204 to 1206
&.is-other {
font-style: italic;
}
Copy link
Member

@simison simison Jan 7, 2026

Choose a reason for hiding this comment

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

Let's instead allow italic & bold styling for option field (can be separate PR) so that this is something users can customize, instead of us dictating the fixed style.

In the theme I tested this didn't even work likely due theme override.

Copilot AI review requested due to automatic review settings January 7, 2026 11:08
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Comment on lines +77 to +85
if ( type === 'radio' && value && value.includes( ': ' ) ) {
// Check if this might be an "Other" value (has the pattern "Label: text")
const colonIndex = value.indexOf( ': ' );
const possibleLabel = value.substring( 0, colonIndex );
// Simple heuristic: if the label part is short (< 30 chars), it might be an "Other" label
if ( possibleLabel.length < 30 ) {
isOtherSelected = true;
otherLabel = possibleLabel;
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The heuristic for detecting "Other" values based on a colon separator and a 30-character label length is fragile. This could incorrectly identify legitimate radio values that happen to contain a colon and short text before it (e.g., "Time: Morning", "Status: Active"). This detection should rely on more explicit metadata or field attributes rather than pattern matching the value string.

Copilot uses AI. Check for mistakes.
Comment on lines 1361 to 1362
if ( ! empty( $option['isOther'] ) ) {
$field .= "<input id='" . esc_attr( $radio_id ) . "' type='radio' name='" . esc_attr( $id ) . "' value='" . esc_attr( $radio_value ) . "' data-wp-on--change='actions.onOtherRadioChange' data-other-label='" . esc_attr( $option_label ) . "' " . $class . checked( $option_label, $value, false ) . ' ' . ( $required ? "required aria-required=\'true\'" : '' ) . '/> ';
$has_option_is_other_id = esc_attr( $radio_id );
} else {
$field .= "<input id='" . esc_attr( $radio_id ) . "' type='radio' name='" . esc_attr( $id ) . "' value='" . esc_attr( $radio_value ) . "' data-wp-on--change='actions.onFieldChange' " . $class . checked( $option_label, $value, false ) . ' ' . ( $required ? "required aria-required=\'true\'" : '' ) . '/> ';
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The input elements are concatenated as very long single lines (lines 1362 and 1365), making the code difficult to read and maintain. Consider breaking these into multi-line strings with proper indentation for better readability.

Suggested change
if ( ! empty( $option['isOther'] ) ) {
$field .= "<input id='" . esc_attr( $radio_id ) . "' type='radio' name='" . esc_attr( $id ) . "' value='" . esc_attr( $radio_value ) . "' data-wp-on--change='actions.onOtherRadioChange' data-other-label='" . esc_attr( $option_label ) . "' " . $class . checked( $option_label, $value, false ) . ' ' . ( $required ? "required aria-required=\'true\'" : '' ) . '/> ';
$has_option_is_other_id = esc_attr( $radio_id );
} else {
$field .= "<input id='" . esc_attr( $radio_id ) . "' type='radio' name='" . esc_attr( $id ) . "' value='" . esc_attr( $radio_value ) . "' data-wp-on--change='actions.onFieldChange' " . $class . checked( $option_label, $value, false ) . ' ' . ( $required ? "required aria-required=\'true\'" : '' ) . '/> ';
}
$on_change = 'actions.onFieldChange';
if ( ! empty( $option['isOther'] ) ) {
$on_change = 'actions.onOtherRadioChange';
$has_option_is_other_id = esc_attr( $radio_id );
}
$required_attr = $required ? " required aria-required='true'" : '';
$input_attributes = "id='" . esc_attr( $radio_id ) . "'";
$input_attributes .= " type='radio'";
$input_attributes .= " name='" . esc_attr( $id ) . "'";
$input_attributes .= " value='" . esc_attr( $radio_value ) . "'";
$input_attributes .= " data-wp-on--change='" . $on_change . "'";
if ( ! empty( $option['isOther'] ) ) {
$input_attributes .= " data-other-label='" . esc_attr( $option_label ) . "'";
}
$input_attributes .= ' ' . $class;
$input_attributes .= checked( $option_label, $value, false );
$input_attributes .= $required_attr;
$field .= '<input ' . $input_attributes . ' /> ';

Copilot uses AI. Check for mistakes.
$custom_text = trim( $custom_text );

// For required fields, ensure custom text is not empty
if ( $this->get_attribute( 'required' ) && empty( $custom_text ) ) {
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The validation logic at line 435 checks if the field is required using $this->get_attribute( 'required' ), but this could be inconsistent with the $isRequired context used elsewhere in validation. Consider using a consistent method to check the required status throughout the validation block.

Suggested change
if ( $this->get_attribute( 'required' ) && empty( $custom_text ) ) {
if ( $isRequired && empty( $custom_text ) ) {

Copilot uses AI. Check for mistakes.
id='" . $other_text_id . "'
name='" . esc_attr( $id ) . "-other-text'
type='text'
class='grunion-field'
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The text input element is missing the jetpack-other-text-input class that's defined in the CSS (grunion.scss). The class attribute should include both grunion-field jetpack-other-text-input to ensure both the general field styles and the specific "Other" input styles are applied correctly.

Suggested change
class='grunion-field'
class='grunion-field jetpack-other-text-input'

Copilot uses AI. Check for mistakes.

// Get the text input value
const fieldset = event.target.closest( 'fieldset' );
const otherTextInput = fieldset?.querySelector( '.jetpack-other-text-input' );
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The querySelector .jetpack-other-text-input in lines 396 and 421 won't find any elements because the rendered HTML uses class grunion-field instead. This will cause the text input clearing and focusing features to fail. The class name should match what's rendered in the PHP (class-contact-form-field.php line 1434).

Suggested change
const otherTextInput = fieldset?.querySelector( '.jetpack-other-text-input' );
const otherTextInput = fieldset?.querySelector( '.grunion-field' );

Copilot uses AI. Check for mistakes.
Significance: minor
Type: added

Add 'Other' option support for radio fields with custom text input, including ARIA accessibility, proper validation, and metadata storage for form submissions
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The changelog filename "add-other-option-to-dropdown" mentions "dropdown" but the feature is actually for radio fields, not dropdown/select fields. The filename should be "add-other-option-to-radio-fields" or similar to accurately reflect the feature scope.

Suggested change
Add 'Other' option support for radio fields with custom text input, including ARIA accessibility, proper validation, and metadata storage for form submissions
Add 'Other' option support for dropdown fields with custom text input, including ARIA accessibility, proper validation, and metadata storage for form submissions

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings January 7, 2026 23:12
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Comment on lines +1405 to +1406
if ( ! empty( $option['isOther'] ) && $this->get_attribute( 'allowother' ) ) {
$field .= $this->render_other_input_field( $radio_id, $required, $id, $this->field_styles );
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

In the else branch (lines 1374-1410), the $option variable is a string after being processed by strip_tags() on line 1378. Attempting to access $option['isOther'] as an array on line 1405 will cause a PHP error. This code path is for legacy forms that don't use optionsdata. Since the "Other" functionality requires optionsdata to work properly, this check will always fail and should be removed from this branch.

Copilot uses AI. Check for mistakes.
Comment on lines 1429 to 1459
private function render_other_input_field( $has_option_is_other_id, $required, $id, $field_styles ) {
$other_text_id = esc_attr( $has_option_is_other_id ) . '-other-text';
$other_label_id = esc_attr( $has_option_is_other_id ) . '-other-label';
$other_label_text = __( 'Please specify…', 'jetpack-forms' );
$aria_required_attr = $required ? "aria-required='true'" : '';

// Prepare styles for the text input to match other form fields
$other_input_styles = ! empty( $field_styles ) ? " style='" . esc_attr( $field_styles ) . "' " : '';

// Render text input wrapper with aria-live for screen reader announcements
$field = "<div class='jetpack-other-text-input-wrapper' data-wp-class--is-visible='state.isOtherSelected' role='region' aria-live='polite'>";

// Add a visually-hidden label for screen readers
$field .= "<label id='" . $other_label_id . "' for='" . $other_text_id . "' class='screen-reader-text'>" . esc_html( $other_label_text ) . '</label>';

$field .= "<input
id='" . $other_text_id . "'
name='" . esc_attr( $id ) . "-other-text'
type='text'
class='grunion-field'
" . $other_input_styles . "
placeholder='" . esc_attr( __( 'Please specify…', 'jetpack-forms' ) ) . "'
value=''
aria-labelledby='" . $other_label_id . "'
" . $aria_required_attr . "
data-wp-on--input='actions.onOtherTextInput'
data-wp-bind--disabled='!state.isOtherSelected'
data-wp-class--has-value='state.hasFieldValue' />";
$field .= '</div>';
return $field;
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The indentation is inconsistent - the function body starts with tabs but should align with the rest of the class methods. The opening brace should be at the same indentation level as the 'private' keyword, and the function body should be indented one level from there.

Copilot uses AI. Check for mistakes.
<ToolbarGroup>
<ToolbarButton
onClick={ () => setAttributes( { isOther: ! isOther } ) }
className={ isOther ? 'is-pressed' : undefined }
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The className logic is inconsistent between the standalone (line 84) and non-standalone (line 136) branches. In the standalone branch, clsx is used with a conditional object { 'is-pressed': isOther }, while in the non-standalone branch it uses a ternary expression. For consistency and code maintainability, both should use the same pattern, preferably clsx with the conditional object pattern.

Suggested change
className={ isOther ? 'is-pressed' : undefined }
className={ clsx( { 'is-pressed': isOther } ) }

Copilot uses AI. Check for mistakes.
class='grunion-field'
" . $other_input_styles . "
placeholder='" . esc_attr( __( 'Please specify…', 'jetpack-forms' ) ) . "'
value=''
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

When an "Other" option has a saved value like "Other: custom text", the text input field needs to be pre-populated with "custom text" so users can see and edit their previous response. Currently, the text input always renders with value='' (line 1451). The code should extract the custom text portion from $value when rendering the form with a saved "Other" response and populate it in the text input field.

Copilot uses AI. Check for mistakes.
Comment on lines 1422 to 1431
* @param string $has_option_is_other_id The ID of the "Other" option.
* @param bool $required Whether the main field is required.
* @param string $id The base ID of the main field.
* @param string $field_styles The styles to apply to the text input field.
*
* @return string The HTML for the "Other" text input field.
*/
private function render_other_input_field( $has_option_is_other_id, $required, $id, $field_styles ) {
$other_text_id = esc_attr( $has_option_is_other_id ) . '-other-text';
$other_label_id = esc_attr( $has_option_is_other_id ) . '-other-label';
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The variable name 'has_option_is_other_id' is confusing and doesn't clearly communicate its purpose. It actually represents the radio button ID for the "Other" option, not whether the option exists. Consider renaming it to something clearer like 'other_option_id' or 'other_radio_id'.

Suggested change
* @param string $has_option_is_other_id The ID of the "Other" option.
* @param bool $required Whether the main field is required.
* @param string $id The base ID of the main field.
* @param string $field_styles The styles to apply to the text input field.
*
* @return string The HTML for the "Other" text input field.
*/
private function render_other_input_field( $has_option_is_other_id, $required, $id, $field_styles ) {
$other_text_id = esc_attr( $has_option_is_other_id ) . '-other-text';
$other_label_id = esc_attr( $has_option_is_other_id ) . '-other-label';
* @param string $other_option_id The ID of the "Other" option.
* @param bool $required Whether the main field is required.
* @param string $id The base ID of the main field.
* @param string $field_styles The styles to apply to the text input field.
*
* @return string The HTML for the "Other" text input field.
*/
private function render_other_input_field( $other_option_id, $required, $id, $field_styles ) {
$other_text_id = esc_attr( $other_option_id ) . '-other-text';
$other_label_id = esc_attr( $other_option_id ) . '-other-label';

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings January 8, 2026 21:35
@simison simison force-pushed the add/other-option-to-dropdown branch from db34e2f to 551ae97 Compare January 8, 2026 21:35
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

projects/packages/forms/src/contact-form/class-contact-form-plugin.php:686

  • The 'allowother' attribute is never set on the field when processing block attributes. While individual options have 'isOther' preserved in optionsdata, the field-level 'allowother' attribute that is checked in validation (line 390) and rendering (line 1360) is never populated. This means the "Other" option functionality will not work properly. Add logic in block_attributes_to_shortcode_attributes() to set 'allowother' to true when any option has isOther set to true.
							// Preserve isOther attribute from the option block so
							// server-side rendering can attach special handlers.
							if ( ! empty( $option['attrs']['isOther'] ) ) {
								$option_data['isOther'] = true;
							}

							if ( isset( $option_attrs['class'] ) ) {
								$option_data['class'] = $option_attrs['class'] . ' wp-block-jetpack-option';
							} else {
								$option_data['class'] = 'wp-block-jetpack-option';
							}

							if ( isset( $option_attrs['style'] ) ) {
								$option_data['style'] = $option_attrs['style'];
							}

							$options[]      = $option_label; // Legacy shortcode attribute in case filters are using it.
							$options_data[] = $option_data;
						}
					}

					$atts['options']     = implode( ',', $options );
					$atts['optionsdata'] = \wp_json_encode( $options_data, JSON_UNESCAPED_SLASHES | JSON_HEX_AMP );

Comment on lines +74 to +84
// For radio fields, check if value uses an "Other" pattern
let isOtherSelected = false;
let otherLabel = null;
if ( type === 'radio' && value && value.includes( ': ' ) ) {
// Check if this might be an "Other" value (has the pattern "Label: text")
const colonIndex = value.indexOf( ': ' );
const possibleLabel = value.substring( 0, colonIndex );
// Simple heuristic: if the label part is short (< 30 chars), it might be an "Other" label
if ( possibleLabel.length < 30 ) {
isOtherSelected = true;
otherLabel = possibleLabel;
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The heuristic for detecting "Other" values (checking if label before ": " is < 30 characters) is fragile and could lead to false positives. For example, legitimate radio values like "Product: Widget" or "Color: Blue" could be misinterpreted as "Other" responses. Consider using a more reliable approach, such as checking against the actual options data to determine if an option has isOther set, or storing an explicit flag when "Other" is selected.

Suggested change
// For radio fields, check if value uses an "Other" pattern
let isOtherSelected = false;
let otherLabel = null;
if ( type === 'radio' && value && value.includes( ': ' ) ) {
// Check if this might be an "Other" value (has the pattern "Label: text")
const colonIndex = value.indexOf( ': ' );
const possibleLabel = value.substring( 0, colonIndex );
// Simple heuristic: if the label part is short (< 30 chars), it might be an "Other" label
if ( possibleLabel.length < 30 ) {
isOtherSelected = true;
otherLabel = possibleLabel;
// For radio fields, determine if an "Other" option is selected based on the field options.
let isOtherSelected = false;
let otherLabel = null;
if (
type === 'radio' &&
typeof value === 'string' &&
extra &&
Array.isArray( extra.options )
) {
const matchingOtherOption = extra.options.find(
option =>
option &&
option.isOther &&
typeof option.label === 'string' &&
value.startsWith( option.label + ': ' )
);
if ( matchingOtherOption ) {
isOtherSelected = true;
otherLabel = matchingOtherOption.label;

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +72
otherPlaceholder: {
type: 'string',
default: __( 'Please specify…', 'jetpack-forms' ),
},
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The otherPlaceholder attribute is defined in the block schema but is never actually used in the frontend rendering. The frontend always uses the hardcoded string "Please specify…" from the translation function. Consider either removing the otherPlaceholder attribute if customization isn't needed, or pass it through to the frontend rendering in render_other_input_field().

Suggested change
otherPlaceholder: {
type: 'string',
default: __( 'Please specify…', 'jetpack-forms' ),
},

Copilot uses AI. Check for mistakes.

/* "Other" option text input styles */
.jetpack-other-text-input-wrapper {
margin-left: 1.5em;
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The margin-left property should use the logical property margin-inline-start instead to support RTL languages. This ensures the margin is applied correctly in both LTR and RTL layouts.

Copilot generated this review using guidance from repository custom instructions.
@simison simison force-pushed the add/other-option-to-dropdown branch from 551ae97 to 7561d62 Compare January 9, 2026 08:07
Copilot AI review requested due to automatic review settings January 9, 2026 08:36
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

projects/packages/forms/src/contact-form/class-contact-form-plugin.php:686

  • The allowOther attribute is never set on the field during block-to-shortcode conversion. While the code preserves the isOther attribute for individual options (lines 666-668), it doesn't set the allowOther field-level attribute that is required by the validation logic (line 390 in class-contact-form-field.php) and the feedback processing logic (line 425 in class-feedback.php). This means the "Other" option validation and processing will not work correctly since $allow_other will always be null/false. The code should set $atts['allowOther'] = true when any option has isOther set to true.
					foreach ( $option_blocks as $option ) {
						$option_label = trim( $option['attrs']['label'] ?? '' );

						if ( $option_label ) {
							$option_attrs = self::get_block_support_classes_and_styles( 'jetpack/option', $option['attrs'] );
							$option_data  = array( 'label' => $option_label );

							// Preserve isOther attribute from the option block so
							// server-side rendering can attach special handlers.
							if ( ! empty( $option['attrs']['isOther'] ) ) {
								$option_data['isOther'] = true;
							}

							if ( isset( $option_attrs['class'] ) ) {
								$option_data['class'] = $option_attrs['class'] . ' wp-block-jetpack-option';
							} else {
								$option_data['class'] = 'wp-block-jetpack-option';
							}

							if ( isset( $option_attrs['style'] ) ) {
								$option_data['style'] = $option_attrs['style'];
							}

							$options[]      = $option_label; // Legacy shortcode attribute in case filters are using it.
							$options_data[] = $option_data;
						}
					}

					$atts['options']     = implode( ',', $options );
					$atts['optionsdata'] = \wp_json_encode( $options_data, JSON_UNESCAPED_SLASHES | JSON_HEX_AMP );

// we have an isOther option so we don't render the duplicate
// appended "Other" radio later.
if ( ! empty( $option['isOther'] ) ) {
$field .= "<input id='" . esc_attr( $radio_id ) . "' type='radio' name='" . esc_attr( $id ) . "' value='" . esc_attr( $radio_value ) . "' data-wp-on--change='actions.onOtherRadioChange' data-other-label='" . esc_attr( $option_label ) . "' " . $class . checked( $option_label, $value, false ) . ' ' . ( $required ? "required aria-required=\'true\'" : '' ) . '/> ';
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The checked state logic uses checked( $option_label, $value, false ), which compares the option label (e.g., "Other") with the field value. However, when a user has previously submitted an "Other" option with custom text, the value will be in the format "Other: custom text" (e.g., "Other: Purple with green stripes"). This means the exact match will fail and the "Other" radio button won't be pre-selected when editing a form or viewing a previously submitted value. The comparison should check if the value either equals the label OR starts with the label followed by ": " for "Other" options.

Suggested change
$field .= "<input id='" . esc_attr( $radio_id ) . "' type='radio' name='" . esc_attr( $id ) . "' value='" . esc_attr( $radio_value ) . "' data-wp-on--change='actions.onOtherRadioChange' data-other-label='" . esc_attr( $option_label ) . "' " . $class . checked( $option_label, $value, false ) . ' ' . ( $required ? "required aria-required=\'true\'" : '' ) . '/> ';
$field .= "<input id='" . esc_attr( $radio_id ) . "' type='radio' name='" . esc_attr( $id ) . "' value='" . esc_attr( $radio_value ) . "' data-wp-on--change='actions.onOtherRadioChange' data-other-label='" . esc_attr( $option_label ) . "' " . $class . checked( true, ( $value === $option_label || ( is_string( $value ) && 0 === strpos( $value, $option_label . ': ' ) ) ), false ) . ' ' . ( $required ? "required aria-required=\'true\'" : '' ) . '/> ';

Copilot uses AI. Check for mistakes.
Comment on lines +74 to +86
// For radio fields, check if value uses an "Other" pattern
let isOtherSelected = false;
let otherLabel = null;
if ( type === 'radio' && value && value.includes( ': ' ) ) {
// Check if this might be an "Other" value (has the pattern "Label: text")
const colonIndex = value.indexOf( ': ' );
const possibleLabel = value.substring( 0, colonIndex );
// Simple heuristic: if the label part is short (< 30 chars), it might be an "Other" label
if ( possibleLabel.length < 30 ) {
isOtherSelected = true;
otherLabel = possibleLabel;
}
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The heuristic used to detect "Other" options (checking if label part is < 30 characters) is fragile and could lead to false positives. For example, if a user legitimately enters a value like "Category: Sports" for a non-Other field, it would incorrectly be detected as an Other option. This logic should instead check against the actual field configuration using the allowother attribute and optionsdata to determine if the field has an Other option, rather than relying on a simple string pattern match.

Copilot uses AI. Check for mistakes.
id='" . $other_text_id . "'
name='" . esc_attr( $id ) . "-other-text'
type='text'
class='grunion-field'
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The JavaScript code is trying to select elements with the class .jetpack-other-text-input, but the actual input element rendered in render_other_input_field only has the class grunion-field (line 1448). This mismatch means that the input field will never be found by the JavaScript selectors on lines 396 and 421 in view.js, preventing the "Other" text input from being cleared or focused properly. The input element needs to also include the class jetpack-other-text-input.

Suggested change
class='grunion-field'
class='grunion-field jetpack-other-text-input'

Copilot uses AI. Check for mistakes.
@github-actions github-actions bot added the [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ label Jan 9, 2026
Copilot AI review requested due to automatic review settings January 9, 2026 09:51
@simison simison force-pushed the add/other-option-to-dropdown branch from fc76e32 to a018102 Compare January 9, 2026 09:51
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Comment on lines +398 to +399
otherTextInput.value = '';
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Direct DOM manipulation with otherTextInput.value = '' (line 398) bypasses React's state management and the WordPress Interactivity API. This can cause inconsistencies between the actual DOM state and the application state. Consider using the Interactivity API's state management or triggering a proper input event to update the value through the appropriate handlers.

Copilot uses AI. Check for mistakes.
font-size: var(--jetpack--contact-form--font-size, 16px);

&:focus {
outline: 2px solid;
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The focus outline style uses outline: 2px solid; without specifying a color, which means it will inherit the currentColor. This could result in poor contrast if the text color doesn't provide sufficient contrast against the background. Consider explicitly setting an outline color or using a color with good contrast, such as outline-color: var(--jetpack--contact-form--focus-color, #0073aa); to ensure accessibility compliance with WCAG contrast requirements.

Suggested change
outline: 2px solid;
outline: 2px solid;
outline-color: var(--jetpack--contact-form--focus-color, #0073aa);

Copilot uses AI. Check for mistakes.
Comment on lines +194 to +195
value={ isFocusedOtherPlaceholder ? otherPlaceholder : '' }
placeholder={ otherPlaceholder }
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The input behavior is confusing: the value is cleared on blur (line 194 shows empty string when not focused), but the placeholder shows the stored value. This creates a poor user experience where users can't see what they typed after unfocusing, and they have to refocus to edit. Consider keeping the value visible at all times by using value={ otherPlaceholder } instead of conditionally clearing it.

Suggested change
value={ isFocusedOtherPlaceholder ? otherPlaceholder : '' }
placeholder={ otherPlaceholder }
value={ otherPlaceholder }
placeholder={ otherPlaceholder || __( 'Please specify…', 'jetpack-forms' ) }

Copilot uses AI. Check for mistakes.
Comment on lines +1438 to +1439
// Render text input wrapper with aria-live for screen reader announcements
$field = "<div class='jetpack-other-text-input-wrapper' data-wp-class--is-visible='state.isOtherSelected' role='region' aria-live='polite'>";
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Using aria-live='polite' on a wrapper that toggles visibility can cause screen readers to announce content changes unexpectedly. When the "Other" option is selected and the text input becomes visible, screen readers will announce the entire region contents. This could be disruptive. Consider removing the aria-live attribute and relying on the focus management (which already moves focus to the input) to provide the necessary feedback to screen reader users.

Suggested change
// Render text input wrapper with aria-live for screen reader announcements
$field = "<div class='jetpack-other-text-input-wrapper' data-wp-class--is-visible='state.isOtherSelected' role='region' aria-live='polite'>";
// Render text input wrapper; rely on focus management instead of aria-live for announcements
$field = "<div class='jetpack-other-text-input-wrapper' data-wp-class--is-visible='state.isOtherSelected' role='region'>";

Copilot uses AI. Check for mistakes.
$field .= '</span>';
$field .= '</label>';
if ( ! empty( $option['isOther'] ) ) {
$placeholder = ! empty( $option['otherPlaceholder'] ) ? $option['otherPlaceholder'] : '';
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The $placeholder value from $option['otherPlaceholder'] is passed directly to render_other_input_field() and then output using esc_attr() at line 1450. However, the placeholder value could contain malicious content if it's not properly sanitized when the option block data is created. Ensure that otherPlaceholder is sanitized when the options data is initially processed or add sanitization here before passing to the render function.

Suggested change
$placeholder = ! empty( $option['otherPlaceholder'] ) ? $option['otherPlaceholder'] : '';
$placeholder = ! empty( $option['otherPlaceholder'] ) ? sanitize_text_field( wp_unslash( $option['otherPlaceholder'] ) ) : '';

Copilot uses AI. Check for mistakes.
*
* @return string The HTML for the "Other" text input field.
*/
private function render_other_input_field( $has_option_is_other_id, $required, $id, $field_styles, $placeholder ) {
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The parameter name $has_option_is_other_id is unclear and misleading. This parameter receives a radio button's ID (like 'favoritecolor-other'), not a boolean indicating whether an option "has" the "isOther" property. Consider renaming to $radio_id or $option_id to better reflect what the parameter actually contains.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Block] Contact Form Form block (also see Contact Form label) [Feature] Contact Form [Package] Forms [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Status] Needs Review This PR is ready for review. [Tests] Includes Tests [Type] Enhancement Changes to an existing feature — removing, adding, or changing parts of it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants