Skip to content

Conversation

@enejb
Copy link
Member

@enejb enejb commented Jan 21, 2026

Fixes FORMS-550

Screen.Recording.2026-01-23.at.10.51.59.AM.mov

Proposed changes:

  • Announce the success message right after the form is submitted.
  • Announce the error message right after the form is submitted.

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:

Visit a form.
Turn on VoiceOver on your mac.
Notice that when you submit the form that you hear that the form was submitted.
Notice that if you have any error while submitting the form that you also hear about them.

Copilot AI review requested due to automatic review settings January 21, 2026 14:54
@github-actions
Copy link
Contributor

github-actions bot commented Jan 21, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the fix/forms-voiceover-submission branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack fix/forms-voiceover-submission

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions github-actions bot added [Feature] Contact Form [Package] Forms [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ labels Jan 21, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 21, 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 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.

@github-actions github-actions bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Jan 21, 2026
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 improves accessibility for Jetpack Forms by implementing VoiceOver screen reader announcements when forms are submitted successfully or encounter errors. The changes ensure that users relying on assistive technology receive immediate feedback about form submission outcomes.

Changes:

  • Added focus management to announce success messages via the success heading element
  • Added callback functions to focus on validation and submission error messages for screen reader announcement
  • Enhanced error message containers with proper ARIA attributes (role="alert", tabindex="-1", aria-label)

Reviewed changes

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

File Description
projects/plugins/jetpack/changelog/fix-forms-voiceover-submission Changelog entry for the Jetpack plugin
projects/packages/forms/changelog/fix-forms-voiceover-submission Changelog entry for the Forms package
projects/packages/forms/src/modules/form/view.js Added three callback functions to manage focus for screen reader announcements
projects/packages/forms/src/contact-form/class-contact-form.php Enhanced error and success message HTML with ARIA attributes and unique IDs

Comment on lines 620 to 626
// Move focus to the success heading for screen reader announcement.
if ( context.submissionSuccess && ! context.hasClickedBack ) {
const successHeading = document.getElementById(
`contact-form-success-header-${ context.formHash }`
);
successHeading?.focus();
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The focus management logic added to scrollToWrapper lacks test coverage. Since this file has a corresponding test file at tests/js/modules/form/view.test.js, consider adding tests to verify that the success heading receives focus when submissionSuccess is true and hasClickedBack is false.

Copilot uses AI. Check for mistakes.
Significance: patch
Type: fixed

Announce form submision and errors to VoiceOver
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The changelog entry should end with a period. According to the project's changelog guidelines, entries should be grammatically correct and end with a period.

Copilot generated this review using guidance from repository custom instructions.
Significance: patch
Type: bugfix

Forms: improve VoiceOver for form submissions.
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The changelog entry should end with a period. According to the project's changelog guidelines, entries should be grammatically correct and end with a period.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines 632 to 644
focusOnValidationError() {
if ( state.showFormErrors ) {
const { ref } = getElement();
ref?.focus();
}
},

focusOnSubmissionError() {
if ( state.showSubmissionError ) {
const { ref } = getElement();
ref?.focus();
}
},
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The new callback functions focusOnValidationError and focusOnSubmissionError lack test coverage. Since this file has a corresponding test file at tests/js/modules/form/view.test.js, consider adding tests to verify that these callbacks correctly focus elements when the respective error states are true.

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

jp-launch-control bot commented Jan 21, 2026

Code Coverage Summary

Coverage changed in 3 files.

File Coverage Δ% Δ Uncovered
projects/packages/forms/src/modules/form/view.js 0/321 (0.00%) 0.00% 26 💔
projects/packages/forms/src/contact-form/class-contact-form-field.php 1091/1744 (62.56%) -0.02% 0 💚
projects/packages/forms/src/contact-form/class-contact-form.php 1023/1490 (68.66%) 0.04% 0 💚

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.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings January 21, 2026 18: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 4 out of 4 changed files in this pull request and generated 2 comments.

Significance: patch
Type: fixed

Announce form submission and errors to VoiceOver
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The changelog entry should end with a period according to the project's changelog guidelines.

Copilot generated this review using guidance from repository custom instructions.
enejb and others added 2 commits January 21, 2026 10:26
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings January 21, 2026 19:49
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 4 out of 4 changed files in this pull request and generated 1 comment.

enejb added 5 commits January 22, 2026 16:34
The validation error message was using both role="alert" (which triggers
  automatic screen reader announcement) and programmatic focus (which also
  triggers announcement). This caused VoiceOver to read the error twice.

  Remove role="alert" and rely on focus alone to announce the error message,
  providing a single clear announcement when validation fails.
 The success heading was being focused directly, causing VoiceOver to
  announce "heading level 4" instead of the content. The role="alert"
  wasn't working because the element was hidden with display:none
  initially - alerts only fire when content changes in a visible element.

  Fix by focusing the wrapper div instead, with aria-labelledby pointing
  to the heading. This causes VoiceOver to read the heading content
  without the unhelpful "heading level 4" announcement, while preserving
  the semantic heading structure for document outline.
enejb added 2 commits January 23, 2026 10:39
  The visually-hidden "Warning" text before each field error message
  doesn't add useful information for screen reader users - the error
  message itself is sufficient context. Also add aria-hidden="true" to
  the warning icon for consistency with the form-level error styling.
 Replace aria-errormessage with aria-describedby for field error
  associations. While aria-errormessage is semantically correct for
  error messages, it has inconsistent support across screen readers,
  particularly in Safari/VoiceOver. aria-describedby has broad support
  and ensures error messages are announced when users focus invalid fields.
Copilot AI review requested due to automatic review settings January 23, 2026 18:50
@enejb enejb added [Status] Needs Review This PR is ready for review. and removed [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. labels Jan 23, 2026
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 5 out of 5 changed files in this pull request and generated 2 comments.

Copilot AI review requested due to automatic review settings January 23, 2026 19:38
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 5 out of 5 changed files in this pull request and generated 4 comments.

focusOnSubmissionError() {
if ( state.showSubmissionError ) {
const { ref } = getElement();
ref?.focus();
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

focusOnSubmissionError is triggered via data-wp-watch while state.showSubmissionError may stay true until the timeout clears it. If the watch callback runs on subsequent updates, it can repeatedly steal focus back to the submission error element, disrupting attempts to interact with the form (e.g., correcting fields and resubmitting). Consider focusing only once when the submission error first appears (e.g., gate on a context flag / previous value).

Suggested change
ref?.focus();
if ( ! ref || ref.__hasFocusedOnSubmissionError ) {
return;
}
ref.__hasFocusedOnSubmissionError = true;
ref.focus();

Copilot uses AI. Check for mistakes.
Comment on lines +915 to 918
data-wp-bind--aria-invalid='state.fieldAriaInvalid'
data-wp-bind--value='state.getFieldValue'
aria-errormessage='" . esc_attr( $id ) . '-' . esc_attr( $type ) . "-error-message'
aria-describedby='" . esc_attr( $id ) . '-' . esc_attr( $type ) . "-error-message'
data-wp-on--input='actions.onFieldChange'
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

aria-describedby is built from the input $type (e.g., ...-tel-error-message). For telephone fields in outlined/animated styles, the inline error markup is suppressed and the error div is rendered later using the field type (e.g., ...-telephone-error-message), which can leave aria-describedby pointing at a non-existent ID. This breaks the input → error message association for assistive tech. Consider decoupling the HTML input type from the error-id suffix (so telephone fields can render type="tel" but still reference/render the ...-telephone-error-message ID consistently).

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +236 to +240
get fieldAriaInvalid() {
// Return 'true' for invalid fields, null to remove the attribute entirely.
// Using null instead of false prevents VoiceOver from announcing "invalid" for valid fields.
return state.fieldHasErrors ? 'true' : null;
},
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

This change introduces new accessibility-critical state/callback logic (fieldAriaInvalid, success aria-hidden toggling, and new focus callbacks), but the existing Jest coverage for this module focuses on helper functions only (projects/packages/forms/tests/js/modules/form/view.test.js). Adding unit tests for these new behaviors (at least: fieldAriaInvalid returning 'true' vs removing the attribute, and the one-time focus gating) would help prevent regressions across screen readers and keyboard navigation.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Contributor

Copilot AI commented Jan 23, 2026

@enejb I've opened a new pull request, #46751, to work on those changes. Once the pull request is ready, I'll request review from you.

@enejb
Copy link
Member Author

enejb commented Jan 23, 2026

@claude Please review

@claude
Copy link

claude bot commented Jan 23, 2026

Claude finished @enejb's task in 1m 54s —— View job


Code Review Complete

  • Read changed files
  • Analyze accessibility implementation
  • Review code quality and best practices
  • Check for security issues
  • Provide comprehensive feedback

Summary

This PR improves VoiceOver accessibility for Jetpack Forms by announcing success and error messages after form submission. The implementation uses focus management to trigger screen reader announcements. While the approach works functionally, there are several accessibility and code quality concerns that should be addressed.

Critical Issues

1. Focus Management Pattern May Trap Keyboard Users 🔴

Location: projects/packages/forms/src/modules/form/view.js:645-656

The focusOnValidationError and focusOnSubmissionError callbacks use data-wp-watch, which runs whenever their watched state is true. This can repeatedly steal focus while users are trying to fix validation errors.

focusOnValidationError() {
    if ( state.showFormErrors ) {
        const { ref } = getElement();
        ref?.focus();  // This can run on every reactive update!
    }
},

Problem: If state.showFormErrors remains true while the user is typing to fix errors, any reactive update could refocus the error message, interrupting their input.

Recommendation: Implement one-time focus logic by tracking whether focus has already been moved for the current error state:

  • Add a context flag like didFocusValidationError
  • Set it to true after focusing once
  • Reset it when errors clear (when state.showFormErrors becomes false)

This is particularly important for the validation error since users need to interact with form fields while the error state is active.

Fix this →

2. Inconsistent aria-describedby References 🔴

Location: projects/packages/forms/src/contact-form/class-contact-form-field.php:1144

The telephone field's aria-describedby attribute points to {$id}-telephone-error-message, but for phone fields with country selectors, the error div is generated with {$id}-phone-error-message (via get_error_div( $id, 'phone' )).

aria-describedby="<?php echo esc_attr( $id ); ?>-telephone-error-message"
// But the actual error div has id: {$id}-phone-error-message

Impact: Breaks the ARIA relationship between input and error message for screen readers.

Note: I see copilot-swe-agent opened PR #46751 to address this - that's great! However, there's a broader issue here about the type="tel" vs field type "telephone" vs "phone" inconsistency that should also be reviewed in that PR.

3. Missing Test Coverage for New Accessibility Features 🟡

Location: projects/packages/forms/tests/js/modules/form/view.test.js

The test file only covers helper functions (getImages, getUrl, maybeTransformValue). The new accessibility-critical features are untested:

  • fieldAriaInvalid returning 'true' vs null
  • isSuccessMessageAriaHidden behavior
  • focusOnValidationError and focusOnSubmissionError callbacks
  • scrollToWrapper success message focus logic

Recommendation: Add tests for these features to prevent regressions:

describe('Accessibility features', () => {
  test('fieldAriaInvalid returns "true" for invalid fields', () => { ... });
  test('fieldAriaInvalid returns null for valid fields', () => { ... });
  test('success message is hidden when not successful', () => { ... });
});

Fix this →

Quality Issues

4. aria-invalid Pattern Inconsistency 🟡

The change from data-wp-bind--aria-invalid='state.fieldHasErrors' to data-wp-bind--aria-invalid='state.fieldAriaInvalid' is applied across all fields, but the implementation could be simplified.

The computed property fieldAriaInvalid returns 'true' or null to prevent VoiceOver from announcing "invalid" on valid fields. However, this adds an extra layer of indirection. The pattern is correct, but consider whether the comment in the code is sufficient documentation:

get fieldAriaInvalid() {
    // Return 'true' for invalid fields, null to remove the attribute entirely.
    // Using null instead of false prevents VoiceOver from announcing "invalid" for valid fields.
    return state.fieldHasErrors ? 'true' : null;
},

Question: Does binding to false actually cause VoiceOver to announce "invalid"? If so, this pattern is correct. If not, the extra indirection may be unnecessary.

5. Removed "Warning" Screen Reader Text 🟡

Location: projects/packages/forms/src/contact-form/class-contact-form-field.php:951 and class-contact-form.php:1506

The PR removes the visually-hidden "Warning" text from error icons:

// Before:
<span class="visually-hidden">Warning</span>

// After:
// Removed entirely

The icon is now marked aria-hidden="true", which is correct. However, the warning context may be lost for screen reader users who previously heard "Warning: [error message]".

Question: Is the error message itself clear enough without the "Warning" prefix? For validation errors, this is probably fine since the message is descriptive (e.g., "This field is required"). For submission errors, verify the message text provides sufficient context.

6. Changelog Entry Missing Period 🟡

Location: projects/packages/forms/changelog/fix-forms-voiceover-submission:4

According to the Jetpack changelog guidelines, entries should end with a period:

Announce form submission and errors to VoiceOver.

This should be:

Announce form submission and errors to VoiceOver.

Wait, it already has a period! The Copilot review was incorrect on this point. ✅

The Jetpack plugin changelog is also correct:

Forms: improve VoiceOver for form submissions.

Positive Aspects ✅

  1. Correct use of aria-labelledby for success message: The success wrapper uses aria-labelledby pointing to the heading, which allows VoiceOver to announce the heading content without the "heading level 4" announcement. This is a good pattern.

  2. Proper ARIA attribute values: Using null instead of false for aria-invalid to remove the attribute is the correct approach for VoiceOver.

  3. Appropriate tabindex="-1" usage: Adding tabindex="-1" to non-focusable elements that need to receive focus for announcements is the right pattern.

  4. Good separation of concerns: The fieldAriaInvalid computed property centralizes the logic for determining when fields should be marked invalid.

Recommendations Summary

Must Fix:

  1. Prevent focus trapping by implementing one-time focus logic in focusOnValidationError and focusOnSubmissionError
  2. Ensure aria-describedby consistency for telephone fields (addressed in PR Forms: Decouple HTML input type from error ID suffix for aria-describedby #46751)

Should Fix:
3. Add test coverage for new accessibility features

Consider:
4. Verify error messages are clear without "Warning" prefix
5. Document why fieldAriaInvalid uses 'true'/null instead of true/false

Testing Notes

The PR description says to test with VoiceOver on Mac, which is appropriate. Additional testing recommendations:

  • Test keyboard navigation while validation errors are visible
  • Verify users can tab to and fix fields without focus jumping back to error messages
  • Test with NVDA/JAWS on Windows if possible
  • Verify aria-describedby associations using browser DevTools or screen reader debugging

Overall, this PR makes meaningful accessibility improvements, but the focus management pattern needs refinement to avoid trapping keyboard users. The telephone field ARIA issue should be resolved in the follow-up PR.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings January 23, 2026 20:32
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 5 out of 5 changed files in this pull request and generated 1 comment.

Comment on lines +645 to +649
focusOnValidationError() {
const context = getContext();

if ( state.showFormErrors ) {
// Only move focus once per error episode to avoid trapping keyboard users.
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

New accessibility behavior is introduced here (aria-invalid/aria-hidden value normalization and focus management via focusOnValidationError/focusOnSubmissionError), but there are no unit tests covering these code paths. Since this package already has Jest tests for interactivity modules, please add tests that mock @wordpress/interactivity (getContext/getElement) to assert the new getters and focus callbacks behave as expected.

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

[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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants