Skip to content

Form components: Support async validation #71184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Aug 18, 2025
Merged

Form components: Support async validation #71184

merged 21 commits into from
Aug 18, 2025

Conversation

mirka
Copy link
Member

@mirka mirka commented Aug 13, 2025

Prerequisite for #71161

What?

Adds support for async validation in the validated form components.

This is a breaking change for these private components.

For the scope of this PR, DataForm is only updated to maintain parity with the existing features, i.e. synchronous validation only.

Why?

Sometimes we'll want to show validation results from the sever.

How?

Separates the customValidator prop into an onValidate callback and a customValidityMessage object. The intent is for this to be flexible enough to support different data fetching systems.

Testing Instructions

npm run storybook:dev and see the stories for the Validated Form Controls.

The async story is on the Overview page.

Code review

It looks like a lot, but the bulk of the code lines are just simple changes to update existing usages to the new ControlWithError component API. The important changes are in the following three files:

Screenshots or screencast

CleanShot.2025-08-13.at.14-03-43.mp4

@mirka mirka self-assigned this Aug 13, 2025
@mirka mirka added [Type] Enhancement A suggestion for improvement. [Package] Components /packages/components labels Aug 13, 2025
@mirka mirka added the Needs Design Feedback Needs general design feedback. label Aug 13, 2025
Copy link

github-actions bot commented Aug 13, 2025

Size Change: +430 B (+0.02%)

Total Size: 1.92 MB

Filename Size Change
build/components/index.min.js 250 kB +209 B (+0.08%)
build/components/style-rtl.css 13.6 kB +61 B (+0.45%)
build/components/style.css 13.6 kB +64 B (+0.47%)
build/edit-site/index.min.js 236 kB +52 B (+0.02%)
build/editor/index.min.js 127 kB +44 B (+0.03%)
ℹ️ View Unchanged
Filename Size
build-module/a11y/index.min.js 482 B
build-module/block-library/accordions/view.min.js 427 B
build-module/block-library/file/view.min.js 466 B
build-module/block-library/form/view.min.js 533 B
build-module/block-library/image/view.min.js 1.78 kB
build-module/block-library/navigation/view.min.js 1.19 kB
build-module/block-library/query/view.min.js 767 B
build-module/block-library/search/view.min.js 639 B
build-module/interactivity-router/full-page.min.js 565 B
build-module/interactivity-router/index.min.js 11.4 kB
build-module/interactivity/debug.min.js 17.5 kB
build-module/interactivity/index.min.js 13.9 kB
build/a11y/index.min.js 952 B
build/annotations/index.min.js 2.13 kB
build/api-fetch/index.min.js 2.4 kB
build/autop/index.min.js 2.12 kB
build/blob/index.min.js 579 B
build/block-directory/index.min.js 7.18 kB
build/block-directory/style-rtl.css 1.03 kB
build/block-directory/style.css 1.03 kB
build/block-editor/content-rtl.css 4.43 kB
build/block-editor/content.css 4.42 kB
build/block-editor/default-editor-styles-rtl.css 392 B
build/block-editor/default-editor-styles.css 392 B
build/block-editor/index.min.js 266 kB
build/block-editor/style-rtl.css 15.9 kB
build/block-editor/style.css 15.9 kB
build/block-library/blocks/accordions/style-rtl.css 591 B
build/block-library/blocks/accordions/style.css 590 B
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 61 B
build/block-library/blocks/archives/style-rtl.css 90 B
build/block-library/blocks/archives/style.css 90 B
build/block-library/blocks/audio/editor-rtl.css 149 B
build/block-library/blocks/audio/editor.css 151 B
build/block-library/blocks/audio/style-rtl.css 132 B
build/block-library/blocks/audio/style.css 132 B
build/block-library/blocks/audio/theme-rtl.css 134 B
build/block-library/blocks/audio/theme.css 134 B
build/block-library/blocks/avatar/editor-rtl.css 115 B
build/block-library/blocks/avatar/editor.css 115 B
build/block-library/blocks/avatar/style-rtl.css 104 B
build/block-library/blocks/avatar/style.css 104 B
build/block-library/blocks/button/editor-rtl.css 265 B
build/block-library/blocks/button/editor.css 265 B
build/block-library/blocks/button/style-rtl.css 554 B
build/block-library/blocks/button/style.css 554 B
build/block-library/blocks/buttons/editor-rtl.css 291 B
build/block-library/blocks/buttons/editor.css 291 B
build/block-library/blocks/buttons/style-rtl.css 349 B
build/block-library/blocks/buttons/style.css 349 B
build/block-library/blocks/calendar/style-rtl.css 239 B
build/block-library/blocks/calendar/style.css 239 B
build/block-library/blocks/categories/editor-rtl.css 132 B
build/block-library/blocks/categories/editor.css 131 B
build/block-library/blocks/categories/style-rtl.css 152 B
build/block-library/blocks/categories/style.css 152 B
build/block-library/blocks/code/editor-rtl.css 53 B
build/block-library/blocks/code/editor.css 53 B
build/block-library/blocks/code/style-rtl.css 139 B
build/block-library/blocks/code/style.css 139 B
build/block-library/blocks/code/theme-rtl.css 122 B
build/block-library/blocks/code/theme.css 122 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 420 B
build/block-library/blocks/columns/style.css 420 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 124 B
build/block-library/blocks/comment-author-avatar/editor.css 124 B
build/block-library/blocks/comment-author-name/style-rtl.css 72 B
build/block-library/blocks/comment-author-name/style.css 72 B
build/block-library/blocks/comment-content/style-rtl.css 120 B
build/block-library/blocks/comment-content/style.css 120 B
build/block-library/blocks/comment-date/style-rtl.css 65 B
build/block-library/blocks/comment-date/style.css 65 B
build/block-library/blocks/comment-edit-link/style-rtl.css 70 B
build/block-library/blocks/comment-edit-link/style.css 70 B
build/block-library/blocks/comment-reply-link/style-rtl.css 71 B
build/block-library/blocks/comment-reply-link/style.css 71 B
build/block-library/blocks/comment-template/style-rtl.css 191 B
build/block-library/blocks/comment-template/style.css 191 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 168 B
build/block-library/blocks/comments-pagination/editor.css 168 B
build/block-library/blocks/comments-pagination/style-rtl.css 201 B
build/block-library/blocks/comments-pagination/style.css 201 B
build/block-library/blocks/comments-title/editor-rtl.css 75 B
build/block-library/blocks/comments-title/editor.css 75 B
build/block-library/blocks/comments/editor-rtl.css 842 B
build/block-library/blocks/comments/editor.css 842 B
build/block-library/blocks/comments/style-rtl.css 637 B
build/block-library/blocks/comments/style.css 637 B
build/block-library/blocks/cover/editor-rtl.css 631 B
build/block-library/blocks/cover/editor.css 631 B
build/block-library/blocks/cover/style-rtl.css 1.7 kB
build/block-library/blocks/cover/style.css 1.69 kB
build/block-library/blocks/details/editor-rtl.css 65 B
build/block-library/blocks/details/editor.css 65 B
build/block-library/blocks/details/style-rtl.css 86 B
build/block-library/blocks/details/style.css 86 B
build/block-library/blocks/embed/editor-rtl.css 331 B
build/block-library/blocks/embed/editor.css 331 B
build/block-library/blocks/embed/style-rtl.css 419 B
build/block-library/blocks/embed/style.css 419 B
build/block-library/blocks/embed/theme-rtl.css 133 B
build/block-library/blocks/embed/theme.css 133 B
build/block-library/blocks/file/editor-rtl.css 326 B
build/block-library/blocks/file/editor.css 326 B
build/block-library/blocks/file/style-rtl.css 278 B
build/block-library/blocks/file/style.css 278 B
build/block-library/blocks/footnotes/style-rtl.css 198 B
build/block-library/blocks/footnotes/style.css 197 B
build/block-library/blocks/form-input/editor-rtl.css 229 B
build/block-library/blocks/form-input/editor.css 229 B
build/block-library/blocks/form-input/style-rtl.css 349 B
build/block-library/blocks/form-input/style.css 349 B
build/block-library/blocks/form-submission-notification/editor-rtl.css 344 B
build/block-library/blocks/form-submission-notification/editor.css 341 B
build/block-library/blocks/form-submit-button/style-rtl.css 69 B
build/block-library/blocks/form-submit-button/style.css 69 B
build/block-library/blocks/freeform/editor-rtl.css 2.59 kB
build/block-library/blocks/freeform/editor.css 2.59 kB
build/block-library/blocks/gallery/editor-rtl.css 615 B
build/block-library/blocks/gallery/editor.css 616 B
build/block-library/blocks/gallery/style-rtl.css 1.83 kB
build/block-library/blocks/gallery/style.css 1.83 kB
build/block-library/blocks/gallery/theme-rtl.css 108 B
build/block-library/blocks/gallery/theme.css 108 B
build/block-library/blocks/group/editor-rtl.css 334 B
build/block-library/blocks/group/editor.css 334 B
build/block-library/blocks/group/style-rtl.css 103 B
build/block-library/blocks/group/style.css 103 B
build/block-library/blocks/group/theme-rtl.css 79 B
build/block-library/blocks/group/theme.css 79 B
build/block-library/blocks/heading/style-rtl.css 188 B
build/block-library/blocks/heading/style.css 188 B
build/block-library/blocks/html/editor-rtl.css 353 B
build/block-library/blocks/html/editor.css 354 B
build/block-library/blocks/image/editor-rtl.css 763 B
build/block-library/blocks/image/editor.css 763 B
build/block-library/blocks/image/style-rtl.css 1.6 kB
build/block-library/blocks/image/style.css 1.59 kB
build/block-library/blocks/image/theme-rtl.css 137 B
build/block-library/blocks/image/theme.css 137 B
build/block-library/blocks/latest-comments/style-rtl.css 355 B
build/block-library/blocks/latest-comments/style.css 354 B
build/block-library/blocks/latest-posts/editor-rtl.css 139 B
build/block-library/blocks/latest-posts/editor.css 138 B
build/block-library/blocks/latest-posts/style-rtl.css 520 B
build/block-library/blocks/latest-posts/style.css 520 B
build/block-library/blocks/list/style-rtl.css 107 B
build/block-library/blocks/list/style.css 107 B
build/block-library/blocks/loginout/style-rtl.css 61 B
build/block-library/blocks/loginout/style.css 61 B
build/block-library/blocks/media-text/editor-rtl.css 321 B
build/block-library/blocks/media-text/editor.css 320 B
build/block-library/blocks/media-text/style-rtl.css 543 B
build/block-library/blocks/media-text/style.css 542 B
build/block-library/blocks/more/editor-rtl.css 393 B
build/block-library/blocks/more/editor.css 393 B
build/block-library/blocks/navigation-link/editor-rtl.css 566 B
build/block-library/blocks/navigation-link/editor.css 568 B
build/block-library/blocks/navigation-link/style-rtl.css 192 B
build/block-library/blocks/navigation-link/style.css 191 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 295 B
build/block-library/blocks/navigation-submenu/editor.css 294 B
build/block-library/blocks/navigation/editor-rtl.css 2.23 kB
build/block-library/blocks/navigation/editor.css 2.24 kB
build/block-library/blocks/navigation/style-rtl.css 2.27 kB
build/block-library/blocks/navigation/style.css 2.26 kB
build/block-library/blocks/nextpage/editor-rtl.css 392 B
build/block-library/blocks/nextpage/editor.css 392 B
build/block-library/blocks/page-list/editor-rtl.css 356 B
build/block-library/blocks/page-list/editor.css 356 B
build/block-library/blocks/page-list/style-rtl.css 192 B
build/block-library/blocks/page-list/style.css 192 B
build/block-library/blocks/paragraph/editor-rtl.css 251 B
build/block-library/blocks/paragraph/editor.css 251 B
build/block-library/blocks/paragraph/style-rtl.css 341 B
build/block-library/blocks/paragraph/style.css 340 B
build/block-library/blocks/post-author-biography/style-rtl.css 74 B
build/block-library/blocks/post-author-biography/style.css 74 B
build/block-library/blocks/post-author-name/style-rtl.css 69 B
build/block-library/blocks/post-author-name/style.css 69 B
build/block-library/blocks/post-author/style-rtl.css 188 B
build/block-library/blocks/post-author/style.css 189 B
build/block-library/blocks/post-comments-count/style-rtl.css 72 B
build/block-library/blocks/post-comments-count/style.css 72 B
build/block-library/blocks/post-comments-form/editor-rtl.css 96 B
build/block-library/blocks/post-comments-form/editor.css 96 B
build/block-library/blocks/post-comments-form/style-rtl.css 527 B
build/block-library/blocks/post-comments-form/style.css 528 B
build/block-library/blocks/post-comments-link/style-rtl.css 71 B
build/block-library/blocks/post-comments-link/style.css 71 B
build/block-library/blocks/post-content/style-rtl.css 61 B
build/block-library/blocks/post-content/style.css 61 B
build/block-library/blocks/post-date/style-rtl.css 62 B
build/block-library/blocks/post-date/style.css 62 B
build/block-library/blocks/post-excerpt/editor-rtl.css 71 B
build/block-library/blocks/post-excerpt/editor.css 71 B
build/block-library/blocks/post-excerpt/style-rtl.css 155 B
build/block-library/blocks/post-excerpt/style.css 155 B
build/block-library/blocks/post-featured-image/editor-rtl.css 715 B
build/block-library/blocks/post-featured-image/editor.css 712 B
build/block-library/blocks/post-featured-image/style-rtl.css 347 B
build/block-library/blocks/post-featured-image/style.css 347 B
build/block-library/blocks/post-navigation-link/style-rtl.css 215 B
build/block-library/blocks/post-navigation-link/style.css 214 B
build/block-library/blocks/post-template/style-rtl.css 414 B
build/block-library/blocks/post-template/style.css 414 B
build/block-library/blocks/post-terms/style-rtl.css 96 B
build/block-library/blocks/post-terms/style.css 96 B
build/block-library/blocks/post-time-to-read/style-rtl.css 70 B
build/block-library/blocks/post-time-to-read/style.css 70 B
build/block-library/blocks/post-title/style-rtl.css 162 B
build/block-library/blocks/post-title/style.css 162 B
build/block-library/blocks/preformatted/style-rtl.css 125 B
build/block-library/blocks/preformatted/style.css 125 B
build/block-library/blocks/pullquote/editor-rtl.css 133 B
build/block-library/blocks/pullquote/editor.css 133 B
build/block-library/blocks/pullquote/style-rtl.css 365 B
build/block-library/blocks/pullquote/style.css 365 B
build/block-library/blocks/pullquote/theme-rtl.css 176 B
build/block-library/blocks/pullquote/theme.css 176 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 121 B
build/block-library/blocks/query-pagination-numbers/editor.css 118 B
build/block-library/blocks/query-pagination/editor-rtl.css 154 B
build/block-library/blocks/query-pagination/editor.css 154 B
build/block-library/blocks/query-pagination/style-rtl.css 237 B
build/block-library/blocks/query-pagination/style.css 237 B
build/block-library/blocks/query-title/style-rtl.css 64 B
build/block-library/blocks/query-title/style.css 64 B
build/block-library/blocks/query-total/style-rtl.css 64 B
build/block-library/blocks/query-total/style.css 64 B
build/block-library/blocks/query/editor-rtl.css 404 B
build/block-library/blocks/query/editor.css 404 B
build/block-library/blocks/quote/style-rtl.css 238 B
build/block-library/blocks/quote/style.css 238 B
build/block-library/blocks/quote/theme-rtl.css 233 B
build/block-library/blocks/quote/theme.css 236 B
build/block-library/blocks/read-more/style-rtl.css 131 B
build/block-library/blocks/read-more/style.css 131 B
build/block-library/blocks/rss/editor-rtl.css 126 B
build/block-library/blocks/rss/editor.css 126 B
build/block-library/blocks/rss/style-rtl.css 284 B
build/block-library/blocks/rss/style.css 283 B
build/block-library/blocks/search/editor-rtl.css 199 B
build/block-library/blocks/search/editor.css 199 B
build/block-library/blocks/search/style-rtl.css 674 B
build/block-library/blocks/search/style.css 671 B
build/block-library/blocks/search/theme-rtl.css 113 B
build/block-library/blocks/search/theme.css 113 B
build/block-library/blocks/separator/editor-rtl.css 100 B
build/block-library/blocks/separator/editor.css 100 B
build/block-library/blocks/separator/style-rtl.css 248 B
build/block-library/blocks/separator/style.css 248 B
build/block-library/blocks/separator/theme-rtl.css 195 B
build/block-library/blocks/separator/theme.css 195 B
build/block-library/blocks/shortcode/editor-rtl.css 286 B
build/block-library/blocks/shortcode/editor.css 286 B
build/block-library/blocks/site-logo/editor-rtl.css 773 B
build/block-library/blocks/site-logo/editor.css 770 B
build/block-library/blocks/site-logo/style-rtl.css 218 B
build/block-library/blocks/site-logo/style.css 218 B
build/block-library/blocks/site-tagline/editor-rtl.css 87 B
build/block-library/blocks/site-tagline/editor.css 87 B
build/block-library/blocks/site-tagline/style-rtl.css 65 B
build/block-library/blocks/site-tagline/style.css 65 B
build/block-library/blocks/site-title/editor-rtl.css 85 B
build/block-library/blocks/site-title/editor.css 85 B
build/block-library/blocks/site-title/style-rtl.css 143 B
build/block-library/blocks/site-title/style.css 143 B
build/block-library/blocks/social-link/editor-rtl.css 314 B
build/block-library/blocks/social-link/editor.css 314 B
build/block-library/blocks/social-links/editor-rtl.css 339 B
build/block-library/blocks/social-links/editor.css 338 B
build/block-library/blocks/social-links/style-rtl.css 1.51 kB
build/block-library/blocks/social-links/style.css 1.51 kB
build/block-library/blocks/spacer/editor-rtl.css 346 B
build/block-library/blocks/spacer/editor.css 346 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table-of-contents/style-rtl.css 83 B
build/block-library/blocks/table-of-contents/style.css 83 B
build/block-library/blocks/table/editor-rtl.css 394 B
build/block-library/blocks/table/editor.css 394 B
build/block-library/blocks/table/style-rtl.css 640 B
build/block-library/blocks/table/style.css 639 B
build/block-library/blocks/table/theme-rtl.css 152 B
build/block-library/blocks/table/theme.css 152 B
build/block-library/blocks/tag-cloud/editor-rtl.css 92 B
build/block-library/blocks/tag-cloud/editor.css 92 B
build/block-library/blocks/tag-cloud/style-rtl.css 248 B
build/block-library/blocks/tag-cloud/style.css 248 B
build/block-library/blocks/template-part/editor-rtl.css 368 B
build/block-library/blocks/template-part/editor.css 368 B
build/block-library/blocks/template-part/theme-rtl.css 113 B
build/block-library/blocks/template-part/theme.css 113 B
build/block-library/blocks/term-description/style-rtl.css 126 B
build/block-library/blocks/term-description/style.css 126 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 165 B
build/block-library/blocks/text-columns/style.css 165 B
build/block-library/blocks/verse/style-rtl.css 98 B
build/block-library/blocks/verse/style.css 98 B
build/block-library/blocks/video/editor-rtl.css 413 B
build/block-library/blocks/video/editor.css 414 B
build/block-library/blocks/video/style-rtl.css 202 B
build/block-library/blocks/video/style.css 202 B
build/block-library/blocks/video/theme-rtl.css 134 B
build/block-library/blocks/video/theme.css 134 B
build/block-library/classic-rtl.css 179 B
build/block-library/classic.css 179 B
build/block-library/common-rtl.css 1.08 kB
build/block-library/common.css 1.08 kB
build/block-library/editor-elements-rtl.css 75 B
build/block-library/editor-elements.css 75 B
build/block-library/editor-rtl.css 11.4 kB
build/block-library/editor.css 11.4 kB
build/block-library/elements-rtl.css 54 B
build/block-library/elements.css 54 B
build/block-library/index.min.js 233 kB
build/block-library/reset-rtl.css 472 B
build/block-library/reset.css 472 B
build/block-library/style-rtl.css 15.4 kB
build/block-library/style.css 15.4 kB
build/block-library/theme-rtl.css 715 B
build/block-library/theme.css 719 B
build/block-serialization-default-parser/index.min.js 1.12 kB
build/block-serialization-spec-parser/index.min.js 2.87 kB
build/blocks/index.min.js 52.6 kB
build/commands/index.min.js 16.2 kB
build/commands/style-rtl.css 956 B
build/commands/style.css 953 B
build/compose/index.min.js 12.8 kB
build/core-commands/index.min.js 3.09 kB
build/core-data/index.min.js 74.9 kB
build/customize-widgets/index.min.js 11 kB
build/customize-widgets/style-rtl.css 1.43 kB
build/customize-widgets/style.css 1.43 kB
build/data-controls/index.min.js 641 B
build/data/index.min.js 8.67 kB
build/date/index.min.js 18 kB
build/deprecated/index.min.js 458 B
build/dom-ready/index.min.js 325 B
build/dom/index.min.js 4.68 kB
build/edit-post/classic-rtl.css 577 B
build/edit-post/classic.css 578 B
build/edit-post/index.min.js 13.4 kB
build/edit-post/style-rtl.css 2.69 kB
build/edit-post/style.css 2.69 kB
build/edit-site/posts-rtl.css 8.85 kB
build/edit-site/posts.css 8.86 kB
build/edit-site/style-rtl.css 14.9 kB
build/edit-site/style.css 14.9 kB
build/edit-widgets/index.min.js 17.8 kB
build/edit-widgets/style-rtl.css 4.05 kB
build/edit-widgets/style.css 4.06 kB
build/editor/style-rtl.css 9.2 kB
build/editor/style.css 9.21 kB
build/element/index.min.js 4.82 kB
build/escape-html/index.min.js 537 B
build/format-library/index.min.js 8.23 kB
build/format-library/style-rtl.css 472 B
build/format-library/style.css 472 B
build/hooks/index.min.js 1.65 kB
build/html-entities/index.min.js 467 B
build/i18n/index.min.js 2.23 kB
build/is-shallow-equal/index.min.js 526 B
build/keyboard-shortcuts/index.min.js 1.31 kB
build/keycodes/index.min.js 1.46 kB
build/list-reusable-blocks/index.min.js 2.13 kB
build/list-reusable-blocks/style-rtl.css 847 B
build/list-reusable-blocks/style.css 848 B
build/media-utils/index.min.js 3.69 kB
build/notices/index.min.js 946 B
build/nux/index.min.js 1.62 kB
build/nux/style-rtl.css 767 B
build/nux/style.css 763 B
build/patterns/index.min.js 7.36 kB
build/patterns/style-rtl.css 687 B
build/patterns/style.css 685 B
build/plugins/index.min.js 1.86 kB
build/preferences-persistence/index.min.js 2.06 kB
build/preferences/index.min.js 2.9 kB
build/preferences/style-rtl.css 562 B
build/preferences/style.css 562 B
build/primitives/index.min.js 829 B
build/priority-queue/index.min.js 1.54 kB
build/private-apis/index.min.js 978 B
build/react-i18n/index.min.js 630 B
build/react-refresh-entry/index.min.js 9.47 kB
build/react-refresh-runtime/index.min.js 6.76 kB
build/redux-routine/index.min.js 2.7 kB
build/reusable-blocks/index.min.js 2.53 kB
build/reusable-blocks/style-rtl.css 255 B
build/reusable-blocks/style.css 255 B
build/rich-text/index.min.js 12.2 kB
build/router/index.min.js 5.44 kB
build/server-side-render/index.min.js 1.6 kB
build/shortcode/index.min.js 1.4 kB
build/style-engine/index.min.js 2.04 kB
build/token-list/index.min.js 581 B
build/url/index.min.js 3.97 kB
build/vendors/react-dom.min.js 41.7 kB
build/vendors/react-jsx-runtime.min.js 556 B
build/vendors/react.min.js 4.02 kB
build/viewport/index.min.js 965 B
build/vips/index.min.js 36.2 kB
build/warning/index.min.js 250 B
build/widgets/index.min.js 7.16 kB
build/widgets/style-rtl.css 1.16 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.04 kB

compressed-size-action

@jameskoster
Copy link
Contributor

Nice, I reckon this works quite well. Instead of an icon, do you think it would be feasible to show a small Spinner (with the same footprint as the icon)?

@mirka
Copy link
Member Author

mirka commented Aug 13, 2025

Something like this?

CleanShot.2025-08-14.at.06-00-24.mp4

I guess 12px is about the smallest we can go for this spinner?

Validating indicator

@mirka mirka added the [Package] DataViews /packages/dataviews label Aug 13, 2025
@mirka
Copy link
Member Author

mirka commented Aug 14, 2025

I added some logic so the validating state only shows after 1s (Nielsen guidelines, but delay time can be tweaked if necessary).

@jameskoster
Copy link
Contributor

LGTM! :)

// TODO: Technically, we could add an optional `customValidity` string prop so the consumer can set
// an error message at any point in time. We should wait until we have a use case though.
customValidator?: ( currentValue: V ) => string | void;
customValidityMessage?: {
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't love this customValidityMessage name, because it muddles the actual customValidity concept in HTML with our own validating and valid statuses.

But given that most consumers will only be dealing with the invalid state that does indeed manipulate HTML customValidity, I think this is the way to go 😕

Comment on lines 92 to +99
const [ errorMessage, setErrorMessage ] = useState< string | undefined >();
const [ statusMessage, setStatusMessage ] = useState<
| {
type: 'validating' | 'valid';
message?: string;
}
| undefined
>();
Copy link
Member Author

Choose a reason for hiding this comment

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

I considered merging the errorMessage and statusMessage into a single status state, but it ended up being more lines of code and not necessarily easier to reason about 😕

I think this is because statusMessage is arbitrarily set by the consumer, while errorMessage is dictated by HTML-level validity data and should always be prioritized over statusMessage. It's kind of easier to track as separate things, and only deal with prioritization at display time (i.e. when it hits ValidationIndicator).

Comment on lines 135 to 141
// Wait before showing a validating state.
const timer = setTimeout( () => {
setStatusMessage( {
type: 'validating',
message: customValidityMessage.message,
} );
}, 1000 );
Copy link
Member Author

Choose a reason for hiding this comment

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

I also considered handling this at the CSS level, but it turned out that doing it with timers was cleaner.

setErrorMessage( validityTarget?.validationMessage );

setStatusMessage( undefined );
return undefined;
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a weird case where our repo rules conflict. Without a return statement, TS emits a "Not all code paths return a value" warning. If I just do return without an explicit undefined value, eslint wants to remove it for being an unnecessary return statement 😅 Hence the return undefined which looks quite meaningless here.

Copy link
Member

Choose a reason for hiding this comment

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

I was able to fix this by explicitly declaring the return type of the effect callback:

useEffect( (): ReturnType< React.EffectCallback > => {

Ugly but works. For some reason TypeScript infers that the return type is undefined | () => void instead of void | () => void. The first one requires return statements.

Copy link
Member Author

Choose a reason for hiding this comment

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

I will try this, thank you!

Copy link
Member Author

Choose a reason for hiding this comment

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

I've been punting the task of adding tests for these behaviors, since this was kind of temporary and everything is still in flux. But I probably should do it soon (in separate PR).

Comment on lines 133 to 141
/**
* To provide feedback from server-side validation, the `customValidityMessage` prop can be used
* to show additional status indicators while waiting for the server response,
* and after the response is received.
*
* These indicators are intended for asynchronous validation calls that may take more than 1 second to complete.
* They may be unnecessary when responses are generally quick.
*/
export const AsyncValidation: StoryObj< typeof ValidatedInputControl > = {
Copy link
Member Author

Choose a reason for hiding this comment

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

@oandregal I don't know how much we want to abstract this in the DataForm APIs, but at the component level I tried to keep it flexible for now. If real-world use cases start to show patterns, maybe we can abstract it a bit more at the component level as well.

The biggest unknowns for me right now are:

  • What percentage of use cases will need async?
  • What percentage of those are slow enough to need a validating state?

@mirka mirka removed the Needs Design Feedback Needs general design feedback. label Aug 14, 2025
@mirka mirka marked this pull request as ready for review August 14, 2025 20:51
Copy link

github-actions bot commented Aug 14, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: mirka <[email protected]>
Co-authored-by: tyxla <[email protected]>
Co-authored-by: aduth <[email protected]>
Co-authored-by: jameskoster <[email protected]>
Co-authored-by: mtias <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@mirka mirka requested a review from a team August 14, 2025 20:51
@mtias
Copy link
Member

mtias commented Aug 14, 2025

Nice! As an aside, I wonder if we should turn the original icon in the first video into an animation instead of the spinner.

Copy link
Member

@tyxla tyxla left a comment

Choose a reason for hiding this comment

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

Great work @mirka!

Just a few thoughts and comments I was testing and reviewing this.

Comment on lines 233 to 243
<ValidationIndicator
type="invalid"
message={ errorMessage }
/>
) }
{ ! errorMessage && statusMessage && (
<ValidationIndicator
type={ statusMessage.type }
message={ statusMessage.message }
/>
) }
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if the level of customizability of the ValidationIndicator is enough. I somehow expected that the consumer would be able to provide their own ValidationIndicator component if they wish. I think the current spinner or icon + message indicator might be a bit limiting.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm hoping we can keep it standardized here, given how we've been trying to hard line on the standard icons/color in regard to the intent in Badge. The message part is also prone to misuse, similar to labels and aria descriptions, in the sense that it shouldn't contain semantic content like links.

That said, my longer term plan as part of #71196 is to make all the these low-level components available as modular pieces that can be used to build any custom validated control. So when that time comes, yes, I think we can provide avenues for more customization in this area. By that time we should have a better idea of the customization needs as well.

Comment on lines 24 to 27
const ICON = {
valid: published,
invalid: error,
};
Copy link
Member

Choose a reason for hiding this comment

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

That object will never change so it can be declared outside of the component.

Copy link
Member

Choose a reason for hiding this comment

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

I thought about leaving this review comment as well, but half-paused on the idea in considering whether it's better to maybe allocate an object on each render vs. always allocating an object for a component that might never be rendered 🤔

Comment on lines 51 to 54
setCustomValidityMessage( {
type: 'invalid',
message: 'This checkbox may not be checked.',
} );
Copy link
Member

Choose a reason for hiding this comment

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

The naming feels a bit off - we're "setting a validity message" but it's an object which also includes "message" and "type". Should we just call it "validity" instead of "validity message"?

Copy link
Member Author

Choose a reason for hiding this comment

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

That makes a lot of sense, plus it's much shorter! 🙈 Will do that.

Comment on lines 41 to 56
onValidate={ ( newValue: any ) => {
const message = field.isValid?.custom?.(
{
...data,
[ id ]: newValue,
},
field
);

if ( message ) {
setCustomValidityMessage( {
type: 'invalid',
message,
} );
return;
}
Copy link
Member

Choose a reason for hiding this comment

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

This part is very repetitive with the other field types, I wonder if we should abstract.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree. I think some abstraction will be inevitable especially when the async support is added in here. TBD! (cc @oandregal)

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

Couple thoughts, but this looks like a good approach 👍

Comment on lines 126 to 133
if ( ! customValidityMessage?.type ) {
validityTarget?.setCustomValidity( '' );
setErrorMessage( validityTarget?.validationMessage );
setStatusMessage( undefined );
return;
}

switch ( customValidityMessage?.type ) {
Copy link
Member

Choose a reason for hiding this comment

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

The first check is really considering whether customValidityMessage is undefined, right? Since type is expected to always exist if it's defined.

Could we simplify some of these checks to assume if it gets past the first condition, customValidityMessage must be truthy.

Suggested change
if ( ! customValidityMessage?.type ) {
validityTarget?.setCustomValidity( '' );
setErrorMessage( validityTarget?.validationMessage );
setStatusMessage( undefined );
return;
}
switch ( customValidityMessage?.type ) {
if ( ! customValidityMessage ) {
validityTarget?.setCustomValidity( '' );
setErrorMessage( validityTarget?.validationMessage );
setStatusMessage( undefined );
return;
}
switch ( customValidityMessage.type ) {

Copy link
Member Author

Choose a reason for hiding this comment

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

The reason I wanted to avoid referencing the customValidityMessage object directly in this useEffect is so it doesn't need to rerun on object reference changes. But I guess is doesn't matter because getValidityTarget is inlined anyway. At the very least we can remove the second conditional though.

switch ( customValidityMessage?.type ) {
case 'validating': {
// Wait before showing a validating state.
const timer = setTimeout( () => {
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if this should be using some sort of shared ref for the timer and assigning as timerRef.current ??= ... ? If somehow this useEffect were triggered without any meaningful changes (e.g. new getValidityTarget reference because it's defined as an inline function) then the timer could reset unnecessarily.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure how that would work, since the clean up function would always have to clear the timer before the next useEffect run anyway. I may be failing to think of certain edge cases, but in the general case this effect will rerun on every user input value change (due to the unmemoized getValidityTarget), and I think it should indeed keep resetting the timer until the user input value settles down.

In any case, there's a good chance we'll need to tweak these micro-interactions if they don't feel quite right in practice. These are controlled demos, and I still found it tricky 😕

}

return null;
setCustomValidityMessage( undefined );
Copy link
Member

Choose a reason for hiding this comment

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

I wondered if allowing onValidate to return a customValidityMessage object would be a simpler way than requiring the component to maintain their own state, but I guess it's valuable to allow someone to pass customValidityMessage outside just this callback, and trying to manage the possibility of both onValidate return values and a direct customValidityMessage prop would be confusing?

Copy link
Member Author

Choose a reason for hiding this comment

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

Right. I do miss the simplicity of returning a status from onValidate.

The original exploration by André with async callbacks can technically work if we didn't have to customize the "pending" messages, but still a React UI component accepting a Promise prop is a bit strange to me. Basically any UI component data could be fetched async, after all, and the expectation is that the consumer will handle it.

resolve/reject style APIs are also kind of roundabout:

onValidate={ ( value, validating, valid, invalid ) => {
  validating();
  fetchValidationResult( value )
    .then( ok => ok ? valid() : invalid() );
} }
onValidate={ ( value, setStatus ) => {
  setStatus( 'validating' );
  fetchValidationResult( value )
    .then( ok => ok ? setStatus( 'valid' ) : setStatus( 'invalid' ) );
} }

So all in all I feel this is the most straightforward and idiomatic, at least at this level.

Copy link
Member

Choose a reason for hiding this comment

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

Async generators could be another option, but maybe a bit too unfamiliar for most folks to be ergonomic.

onValidate={ async function* () {
  yield { type: 'validating', message: 'Validating...' }
  const ok = await fetchValidationResult( value );
  if ( ok ) {
    yield { type: 'valid', message: 'Validated' };
  } else {
    yield { type: 'invalid', message: 'Error' };
  }
} }

Copy link
Member

Choose a reason for hiding this comment

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

From experience with using async generators in custom data stores, I can tell folks found them a bit unintuitive, complex, and hard to read.

Copy link
Member

Choose a reason for hiding this comment

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

onValidate could return an async iterator, that's exactly the semantics we're looking for here. An async stream of values (statuses) that ends after some time.

The caller could then read the validation progress in a for await loop:

for await ( const validity of onValidate() ) {
  setCustomValidity( validity );
}

Andrew's example async generator returns exactly this kind of iterator.

From experience with using async generators in custom data stores, I can tell folks found them a bit unintuitive, complex, and hard to read.

There is a big difference between data controls and async iterators. In data controls, the generators are used to implement a custom domain specific language, where the yielded objects have special meaning interpreted by a special runtime (redux-routine). Very custom stuff, quirky and poorly documented. On the other hand, async iterators are a core language feature. Should be much easier to get familiar with them. And LLMs should be perfectly fluent in understanding and writing them.

Copy link
Member Author

Choose a reason for hiding this comment

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

it's valuable to allow someone to pass customValidityMessage outside just this callback, and trying to manage the possibility of both onValidate return values and a direct customValidityMessage prop would be confusing

Going back to this part of what Andrew said — handling everything through the onValidate doesn't work well with hook-based data fetching (SWR, TanStack, etc.) for example, so we'll want a way to set the validity state outside of this callback anyway. We can revisit when we have a better idea of how common the async use cases are, and what patterns are used for fetching. Maybe we can introduce an abstraction at a different layer.

@mirka
Copy link
Member Author

mirka commented Aug 15, 2025

I wonder if we should turn the original icon in the first video into an animation instead of the spinner.

Same linear speed as Spinner:

CleanShot.2025-08-16.at.05-08-29.mp4

With a bit more snap:

CleanShot.2025-08-16.at.05-07-25.mp4

Or something else?

@jameskoster
Copy link
Contributor

That looks a bit clunky to me. I think it would work better if the filled section resized, as if each of the straight edges were like hands on a clock face, but that seems like a lot of work. We have a dedicated component for this (Spinner), so I'm not sure why we wouldn't want to use it here? If we don't like the design then we should revisit that holistically.

mirka added 2 commits August 19, 2025 00:24
This reverts commit 88ab560.
# Conflicts:
#	packages/components/CHANGELOG.md
@mirka
Copy link
Member Author

mirka commented Aug 18, 2025

Reverted back to the Spinner for now. Let's reevaluate separately if necessary.

@mirka mirka merged commit 5f56cd1 into trunk Aug 18, 2025
105 of 107 checks passed
@mirka mirka deleted the async-validation branch August 18, 2025 16:33
@github-actions github-actions bot added this to the Gutenberg 21.5 milestone Aug 18, 2025
@oandregal
Copy link
Member

Thanks, I've updated the Dataform API at #71161 to leverage this.

mirka added a commit that referenced this pull request Aug 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Components /packages/components [Package] DataViews /packages/dataviews [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants